Skip to content

Commit

Permalink
Merge pull request #4 from controlplaneio-fluxcd/components-image-update
Browse files Browse the repository at this point in the history
Automated updates for components image digest
  • Loading branch information
stefanprodan authored May 30, 2024
2 parents c696cdf + 199953d commit c09d874
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 91 deletions.
23 changes: 23 additions & 0 deletions api/v1/fluxinstance_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ type Distribution struct {
// +kubebuilder:validation:Enum:=source-controller;kustomize-controller;helm-controller;notification-controller;image-reflector-controller;image-automation-controller
type Component string

// ComponentImage represents a container image used by a component.
type ComponentImage struct {
// Name of the component.
// +required
Name string `json:"name"`

// Repository address of the container image.
// +required
Repository string `json:"repository"`

// Tag of the container image.
// +required
Tag string `json:"tag"`

// Digest of the container image.
// +optional
Digest string `json:"digest,omitempty"`
}

// Cluster is the specification for the Kubernetes cluster.
type Cluster struct {
// Domain is the cluster domain used for generating the FQDN of services.
Expand Down Expand Up @@ -147,6 +166,10 @@ type FluxInstanceStatus struct {
// +optional
LastAppliedRevision string `json:"lastAppliedRevision,omitempty"`

// Components contains the container images used by the components.
// +optional
Components []ComponentImage `json:"components,omitempty"`

// Inventory contains a list of Kubernetes resource object references
// last applied on the cluster.
// +optional
Expand Down
20 changes: 20 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,31 @@ spec:
status:
description: FluxInstanceStatus defines the observed state of FluxInstance
properties:
components:
description: Components contains the container images used by the
components.
items:
description: ComponentImage represents a container image used by
a component.
properties:
digest:
description: Digest of the container image.
type: string
name:
description: Name of the component.
type: string
repository:
description: Repository address of the container image.
type: string
tag:
description: Tag of the container image.
type: string
required:
- name
- repository
- tag
type: object
type: array
conditions:
description: Conditions contains the readiness conditions of the object.
items:
Expand Down
59 changes: 5 additions & 54 deletions internal/builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@ import (
"path"
"path/filepath"
"sort"
"strings"

"github.com/fluxcd/pkg/kustomize"
"github.com/fluxcd/pkg/ssa"
ssautil "github.com/fluxcd/pkg/ssa/utils"
gcname "github.com/google/go-containerregistry/pkg/name"
"github.com/opencontainers/go-digest"
cp "github.com/otiai10/copy"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// Build copies the source directory to a temporary directory, generates the
Expand Down Expand Up @@ -52,10 +49,11 @@ func Build(srcDir, tmpDir string, options Options) (*Result, error) {
d := digest.FromBytes(data)

return &Result{
Version: options.Version,
Objects: objects,
Digest: d.String(),
Revision: fmt.Sprintf("%s@%s", options.Version, d.String()),
Version: options.Version,
Objects: objects,
Digest: d.String(),
Revision: fmt.Sprintf("%s@%s", options.Version, d.String()),
ComponentImages: options.ComponentImages,
}, nil
}

Expand Down Expand Up @@ -103,50 +101,3 @@ func generate(base string, options Options) error {
}
return nil
}

// ComponentImage represents a container image used by a component.
type ComponentImage struct {
Component string
ImageName string
ImageTag string
ImageDigest string
}

// ExtractComponentImages reads the source directory and extracts the container images
// from the components manifests.
func ExtractComponentImages(srcDir string, opts Options) ([]ComponentImage, error) {
images := make([]ComponentImage, len(opts.Components))
for i, component := range opts.Components {
d, err := os.ReadFile(filepath.Join(srcDir, fmt.Sprintf("/%s.yaml", component)))
if err != nil {
return nil, err
}
objects, err := ssautil.ReadObjects(bytes.NewReader(d))
if err != nil {
return nil, err
}
for _, obj := range objects {
if obj.GetKind() == "Deployment" {
containers, ok, _ := unstructured.NestedSlice(obj.Object, "spec", "template", "spec", "containers")
if !ok {
return nil, fmt.Errorf("containers not found in %s", obj.GetName())
}
for _, container := range containers {
img := container.(map[string]interface{})["image"].(string)
tag, err := gcname.NewTag(img, gcname.WeakValidation)
if err != nil {
return nil, err
}

images[i] = ComponentImage{
Component: component,
ImageName: fmt.Sprintf("%s/%s", strings.TrimSuffix(opts.Registry, "/"), component),
ImageTag: tag.Identifier(),
}
}
}
}
}

return images, nil
}
26 changes: 0 additions & 26 deletions internal/builder/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,32 +195,6 @@ func TestBuild_InvalidPatches(t *testing.T) {
g.Expect(err.Error()).To(ContainSubstring("Unexpected kind: removes"))
}

func TestBuild_extractImages(t *testing.T) {
g := NewWithT(t)
const version = "v2.3.0"
srcDir := filepath.Join("testdata", version)

images, err := ExtractComponentImages(srcDir, MakeDefaultOptions())
g.Expect(err).NotTo(HaveOccurred())

t.Log(images)
g.Expect(images).To(HaveLen(6))
g.Expect(images).To(ContainElements(
ComponentImage{
Component: "source-controller",
ImageName: "ghcr.io/fluxcd/source-controller",
ImageTag: "v1.3.0",
ImageDigest: "",
},
ComponentImage{
Component: "kustomize-controller",
ImageName: "ghcr.io/fluxcd/kustomize-controller",
ImageTag: "v1.3.0",
ImageDigest: "",
},
))
}

func testTempDir(t *testing.T) (string, error) {
tmpDir := t.TempDir()

Expand Down
134 changes: 134 additions & 0 deletions internal/builder/images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2024 Stefan Prodan.
// SPDX-License-Identifier: AGPL-3.0

package builder

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/fluxcd/pkg/apis/kustomize"
ssautil "github.com/fluxcd/pkg/ssa/utils"
gcname "github.com/google/go-containerregistry/pkg/name"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
)

// ComponentImage represents a container image used by a component.
type ComponentImage struct {
Name string
Repository string
Tag string
Digest string
}

// ExtractComponentImages reads the source directory and extracts the container images
// from the components manifests.
func ExtractComponentImages(srcDir string, opts Options) ([]ComponentImage, error) {
images := make([]ComponentImage, len(opts.Components))
for i, component := range opts.Components {
d, err := os.ReadFile(filepath.Join(srcDir, fmt.Sprintf("/%s.yaml", component)))
if err != nil {
return nil, err
}
objects, err := ssautil.ReadObjects(bytes.NewReader(d))
if err != nil {
return nil, err
}
for _, obj := range objects {
if obj.GetKind() == "Deployment" {
containers, ok, _ := unstructured.NestedSlice(obj.Object, "spec", "template", "spec", "containers")
if !ok {
return nil, fmt.Errorf("containers not found in %s", obj.GetName())
}
for _, container := range containers {
img := container.(map[string]interface{})["image"].(string)
tag, err := gcname.NewTag(img, gcname.WeakValidation)
if err != nil {
return nil, err
}

images[i] = ComponentImage{
Name: component,
Repository: fmt.Sprintf("%s/%s", strings.TrimSuffix(opts.Registry, "/"), component),
Tag: tag.Identifier(),
}
}
}
}
}

return images, nil
}

// FetchComponentImages fetches the components images from the distribution repository.
func FetchComponentImages(opts Options) (images []ComponentImage, err error) {
registry := strings.TrimSuffix(opts.Registry, "/")
var distro string

switch registry {
case "fluxcd":
distro = "upstream-alpine"
case "ghcr.io/fluxcd":
distro = "upstream-alpine"
case "ghcr.io/controlplaneio-fluxcd/alpine":
distro = "enterprise-alpine"
case "ghcr.io/controlplaneio-fluxcd/distroless":
distro = "enterprise-distroless"
default:
return nil, fmt.Errorf("unsupported registry: %s", registry)
}

const ghRepo = "https://raw.githubusercontent.com/controlplaneio-fluxcd/distribution/main/images"
ghURL := fmt.Sprintf("%s/%s/%s.yaml", ghRepo, opts.Version, distro)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, ghURL, nil)
if err != nil {
return nil, err
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read body: %v", err)
}

var kc struct {
Images []kustomize.Image `yaml:"images"`
}
err = yaml.Unmarshal(data, &kc)
if err != nil {
return nil, err
}

for _, img := range kc.Images {
component := strings.TrimPrefix(img.Name, registry+"/")
if containsItemString(opts.Components, component) {
images = append(images, ComponentImage{
Name: component,
Repository: fmt.Sprintf("%s/%s", registry, component),
Tag: img.NewTag,
Digest: img.Digest,
})
}
}

if len(images) != len(opts.Components) {
return nil, fmt.Errorf("missing images for components: %v", opts.Components)
}
return images, nil
}
Loading

0 comments on commit c09d874

Please sign in to comment.