Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automated updates for components image digest #4

Merged
merged 2 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Contributor

@souleb souleb May 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this should be a parameter? we could then fetch from a test http server in our tests? otherwise tests will fail everytime change the digests upstream.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests will not fail, as they point to the upstream CNCF Flux images (fixed version v2.3.0). I'm reluctant on exposing the URL in the API, as this function should fetch the digests from the OCI repo directly, instead of using Git like now.

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