Skip to content

Commit adae770

Browse files
authored
Merge pull request #21 from michalskrivanek/labels
add checking of running container on each exporter host
2 parents 8f608da + 77c725c commit adae770

File tree

6 files changed

+495
-33
lines changed

6 files changed

+495
-33
lines changed

cmd/apply.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ var applyCmd = &cobra.Command{
6060
}
6161

6262
config_lint.Validate(cfg)
63+
if err := config_lint.ValidateWithError(cfg); err != nil {
64+
return fmt.Errorf("config validation failed: %w", err)
65+
}
66+
6367
tapplier, err := templating.NewTemplateApplier(cfg, nil)
6468
if err != nil {
6569
return fmt.Errorf("error creating template applier %w", err)

internal/config/config.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import (
44
"os"
55
"path/filepath"
66

7+
"github.com/jumpstarter-dev/jumpstarter-lab-config/internal/container"
78
"gopkg.in/yaml.v3"
89
)
910

1011
// Config represents the structure of the jumpstarter-lab.yaml file.
1112
type Config struct {
12-
Sources Sources `yaml:"sources"`
13-
Variables []string `yaml:"variables"`
14-
BaseDir string `yaml:"-"` // Not serialized, set programmatically
15-
Loaded *LoadedLabConfig `yaml:"-"` // Not serialized, used internally
13+
Sources Sources `yaml:"sources"`
14+
Variables []string `yaml:"variables"`
15+
BaseDir string `yaml:"-"` // Not serialized, set programmatically
16+
Loaded *LoadedLabConfig `yaml:"-"` // Not serialized, used internally
17+
ContainerVersions map[string]*container.ImageLabels `yaml:"-"` // Not serialized, container versions by image URL
1618
}
1719

1820
// Sources defines the paths for various configuration files.
@@ -43,6 +45,9 @@ func LoadConfig(filePath string, vaultPassFile string) (*Config, error) {
4345
cfg.BaseDir = filepath.Dir(filePath)
4446

4547
cfg.Loaded, err = LoadAllResources(&cfg, vaultPassFile)
48+
if err != nil {
49+
return nil, err
50+
}
4651

47-
return &cfg, err
52+
return &cfg, nil
4853
}

internal/config/loader.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
jsApi "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1"
1111
api "github.com/jumpstarter-dev/jumpstarter-lab-config/api/v1alpha1"
12+
"github.com/jumpstarter-dev/jumpstarter-lab-config/internal/container"
1213
"github.com/jumpstarter-dev/jumpstarter-lab-config/internal/vars"
1314

1415
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -305,9 +306,45 @@ func LoadAllResources(cfg *Config, vaultPassFile string) (*LoadedLabConfig, erro
305306
}
306307
}
307308

309+
// Retrieve container versions for all unique container images found in exporters
310+
containerVersions := retrieveContainerVersionsFromExporters(loaded)
311+
312+
// Store the container versions in the config
313+
cfg.ContainerVersions = containerVersions
314+
308315
return loaded, nil
309316
}
310317

318+
// retrieveContainerVersionsFromExporters retrieves container versions for all unique container images found in exporters
319+
func retrieveContainerVersionsFromExporters(loaded *LoadedLabConfig) map[string]*container.ImageLabels {
320+
containerVersions := make(map[string]*container.ImageLabels)
321+
uniqueImages := make(map[string]bool)
322+
323+
// Collect all unique container images from exporter config templates
324+
for _, template := range loaded.ExporterConfigTemplates {
325+
if template.Spec.ContainerImage != "" {
326+
uniqueImages[template.Spec.ContainerImage] = true
327+
}
328+
}
329+
330+
// Retrieve version information for each unique image
331+
for imageURL := range uniqueImages {
332+
imageLabels, err := container.GetImageLabelsFromRegistry(imageURL)
333+
if err != nil {
334+
fmt.Printf("Latest container version of %s: unavailable (%v)\n", imageURL, err)
335+
containerVersions[imageURL] = &container.ImageLabels{} // Store empty labels
336+
} else if imageLabels.IsEmpty() {
337+
fmt.Printf("Latest container version of %s: no version info available\n", imageURL)
338+
containerVersions[imageURL] = imageLabels
339+
} else {
340+
fmt.Printf("Latest container version of %s: %s %s\n", imageURL, imageLabels.Version, imageLabels.Revision)
341+
containerVersions[imageURL] = imageLabels
342+
}
343+
}
344+
345+
return containerVersions
346+
}
347+
311348
func ReportLoading(cfg *Config) {
312349

313350
fmt.Println("Reading files from:")

internal/container/version.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package container
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os/exec"
7+
"strings"
8+
)
9+
10+
// ImageLabels represents the labels from a container image
11+
type ImageLabels struct {
12+
Version string
13+
Revision string
14+
}
15+
16+
// GetImageLabelsFromRegistry retrieves image labels from a registry using skopeo
17+
func GetImageLabelsFromRegistry(imageURL string) (*ImageLabels, error) {
18+
// Always add the docker:// prefix for skopeo
19+
imageURL = "docker://" + imageURL
20+
21+
cmd := exec.Command("skopeo", "inspect", "--override-os", "linux", "--override-arch", "amd64", imageURL)
22+
output, err := cmd.Output()
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to inspect image %s with skopeo: %w", imageURL, err)
25+
}
26+
27+
var imageInfo struct {
28+
Labels map[string]string `json:"Labels"`
29+
}
30+
31+
if err := json.Unmarshal(output, &imageInfo); err != nil {
32+
return nil, fmt.Errorf("failed to parse skopeo output: %w", err)
33+
}
34+
35+
// Get both label sets
36+
jumpstarterVersion := imageInfo.Labels["jumpstarter.version"]
37+
jumpstarterRevision := imageInfo.Labels["jumpstarter.revision"]
38+
ociVersion := imageInfo.Labels["org.opencontainers.image.version"]
39+
ociRevision := imageInfo.Labels["org.opencontainers.image.revision"]
40+
41+
// Use jumpstarter labels if both exist, otherwise fall back to OCI labels
42+
var version, revision string
43+
if jumpstarterVersion != "" && jumpstarterRevision != "" {
44+
version = jumpstarterVersion
45+
revision = jumpstarterRevision
46+
} else {
47+
version = ociVersion
48+
revision = ociRevision
49+
}
50+
51+
// Only return empty labels if BOTH version and revision are completely missing
52+
// (neither jumpstarter nor OCI labels exist)
53+
return &ImageLabels{
54+
Version: version,
55+
Revision: revision,
56+
}, nil
57+
}
58+
59+
// GetRunningContainerLabels retrieves labels from a running container using podman inspect
60+
func GetRunningContainerLabels(serviceName string) (*ImageLabels, error) {
61+
cmd := exec.Command("podman", "inspect", "--format",
62+
"{{index .Config.Labels \"jumpstarter.version\"}} {{index .Config.Labels \"jumpstarter.revision\"}}",
63+
serviceName)
64+
output, err := cmd.Output()
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to inspect container %s: %w", serviceName, err)
67+
}
68+
69+
parts := strings.Fields(strings.TrimSpace(string(output)))
70+
if len(parts) < 2 {
71+
return &ImageLabels{}, nil // Return empty labels if not found
72+
}
73+
74+
return &ImageLabels{
75+
Version: parts[0],
76+
Revision: parts[1],
77+
}, nil
78+
}
79+
80+
// CompareVersions compares two ImageLabels and returns true if they match
81+
func (il *ImageLabels) Matches(other *ImageLabels) bool {
82+
return il.Version == other.Version && il.Revision == other.Revision
83+
}
84+
85+
// IsEmpty returns true if both version and revision are empty
86+
func (il *ImageLabels) IsEmpty() bool {
87+
return il.Version == "" && il.Revision == ""
88+
}
89+
90+
// String returns a string representation of the image labels
91+
func (il *ImageLabels) String() string {
92+
if il.IsEmpty() {
93+
return "no version info"
94+
}
95+
return fmt.Sprintf("version=%s revision=%s", il.Version, il.Revision)
96+
}

0 commit comments

Comments
 (0)