diff --git a/README.md b/README.md index 77223d3..0de02b5 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ spec: distribution: version: "2.x" registry: "ghcr.io/fluxcd" + artifact: "oci://ghcr.io/controlplaneio-fluxcd/flux-operator-manifests" components: - source-controller - kustomize-controller diff --git a/api/v1/fluxinstance_types.go b/api/v1/fluxinstance_types.go index 2342a78..857260a 100644 --- a/api/v1/fluxinstance_types.go +++ b/api/v1/fluxinstance_types.go @@ -89,7 +89,7 @@ type Distribution struct { // e.g. 'oci://ghcr.io/controlplaneio-fluxcd/flux-operator-manifests:latest'. // +kubebuilder:validation:Pattern="^oci://.*$" // +optional - Artifact string `json:"manifestsURL,omitempty"` + Artifact string `json:"artifact,omitempty"` } // Component is the name of a controller to install. diff --git a/config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml b/config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml index f24d3d0..a74c353 100644 --- a/config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml +++ b/config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml @@ -105,18 +105,18 @@ spec: description: Distribution specifies the version and container registry to pull images from. properties: - imagePullSecret: - description: |- - ImagePullSecret is the name of the Kubernetes secret - to use for pulling images. - type: string - manifestsURL: + artifact: description: |- Artifact is the URL to the OCI artifact containing the latest Kubernetes manifests for the distribution, e.g. 'oci://ghcr.io/controlplaneio-fluxcd/flux-operator-manifests:latest'. pattern: ^oci://.*$ type: string + imagePullSecret: + description: |- + ImagePullSecret is the name of the Kubernetes secret + to use for pulling images. + type: string registry: description: |- Registry address to pull the distribution images from diff --git a/config/samples/fluxcd_v1_fluxinstance.yaml b/config/samples/fluxcd_v1_fluxinstance.yaml index b13cb42..3d26d27 100644 --- a/config/samples/fluxcd_v1_fluxinstance.yaml +++ b/config/samples/fluxcd_v1_fluxinstance.yaml @@ -6,6 +6,7 @@ spec: distribution: version: "2.x" registry: "ghcr.io/fluxcd" + artifact: "oci://ghcr.io/controlplaneio-fluxcd/flux-operator-manifests:latest" components: - source-controller - kustomize-controller diff --git a/go.mod b/go.mod index e503d6b..f8c7712 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/fluxcd/pkg/kustomize v1.11.0 github.com/fluxcd/pkg/runtime v0.47.1 github.com/fluxcd/pkg/ssa v0.39.1 + github.com/fluxcd/pkg/tar v0.7.0 github.com/google/go-containerregistry v0.19.1 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 @@ -32,7 +33,13 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect @@ -67,9 +74,11 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.5 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.5.0 // indirect @@ -78,6 +87,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/image-spec v1.1.0-rc3 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.19.0 // indirect @@ -85,7 +95,9 @@ require ( github.com/prometheus/common v0.53.0 // indirect github.com/prometheus/procfs v0.14.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.1 // indirect github.com/spf13/cobra v1.8.0 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index f623fa4..4747451 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= @@ -16,13 +17,26 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.0+incompatible h1:z4bf8HvONXX9Tde5lGBMQ7yCJgNahmJumdrStZAbeY4= +github.com/docker/docker v24.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= @@ -47,6 +61,8 @@ github.com/fluxcd/pkg/sourceignore v0.7.0 h1:qQrB2o543wA1o4vgR62ufwkAaDp8+f8Wdj1 github.com/fluxcd/pkg/sourceignore v0.7.0/go.mod h1:A4GuZt2seJJkBm3kMiIx9nheoYZs98KTMr/A6/2fIro= github.com/fluxcd/pkg/ssa v0.39.1 h1:xPYRKqgqB5p+5jgz2xBkXCE/7i1FIOa+nA3Wr7Gu2Ek= github.com/fluxcd/pkg/ssa v0.39.1/go.mod h1:AkhMoFxipMf3WoO3lkXjj2nHNIz6sA5yQ50aBodtxnk= +github.com/fluxcd/pkg/tar v0.7.0 h1:xdg95f4DlzMgd4m+xPRXrX4NLb8P8b5SAqB19sDOLIs= +github.com/fluxcd/pkg/tar v0.7.0/go.mod h1:KLg1zMZF7sEncGA9LEsfkskbCMyLSEgrjBRXqFK++VE= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= @@ -110,6 +126,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -120,6 +138,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= @@ -143,6 +163,8 @@ github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= @@ -168,18 +190,28 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= +github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -216,6 +248,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= @@ -255,6 +289,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= diff --git a/hack/vendor-flux-manifests.sh b/hack/vendor-flux-manifests.sh index 2e61514..de8250c 100755 --- a/hack/vendor-flux-manifests.sh +++ b/hack/vendor-flux-manifests.sh @@ -36,7 +36,7 @@ curl -sLO https://github.com/controlplaneio-fluxcd/distribution/archive/refs/hea tar xzf main.tar.gz -C "${DEST_DIR}" mkdir -p "${IMG_DIR}" -cp -r ${DEST_DIR}/distribution-main/images/* ${IMG_DIR}/ +cp -rf ${DEST_DIR}/distribution-main/images/* ${IMG_DIR}/ rm -rf ${DEST_DIR}/distribution-main rm -rf main.tar.gz info "flux image manifests copied to flux-images" diff --git a/internal/builder/images.go b/internal/builder/images.go index 77d18e0..0b978e5 100644 --- a/internal/builder/images.go +++ b/internal/builder/images.go @@ -5,14 +5,10 @@ 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" @@ -60,8 +56,9 @@ func ExtractComponentImages(srcDir string, opts Options) ([]ComponentImage, erro return images, nil } -// FetchComponentImages fetches the components images from the distribution repository. -func FetchComponentImages(opts Options) (images []ComponentImage, err error) { +// ExtractComponentImagesWithDigest reads the source directory and extracts +// the container images with digest from the kustomize images patches. +func ExtractComponentImagesWithDigest(srcDir string, opts Options) (images []ComponentImage, err error) { registry := strings.TrimSuffix(opts.Registry, "/") var distro string @@ -78,23 +75,9 @@ func FetchComponentImages(opts Options) (images []ComponentImage, err error) { 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) + imageFile := fmt.Sprintf("%s/%s/%s.yaml", srcDir, 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) + data, err := os.ReadFile(imageFile) if err != nil { return nil, fmt.Errorf("read body: %v", err) } diff --git a/internal/builder/images_test.go b/internal/builder/images_test.go index d0d8934..a99814d 100644 --- a/internal/builder/images_test.go +++ b/internal/builder/images_test.go @@ -36,14 +36,15 @@ func TestBuild_ExtractImages(t *testing.T) { )) } -func TestBuild_FetchImages(t *testing.T) { +func TestBuild_ExtractImagesWithDigest(t *testing.T) { g := NewWithT(t) const version = "v2.3.0" opts := MakeDefaultOptions() opts.Version = version opts.Registry = "ghcr.io/fluxcd" - images, err := FetchComponentImages(opts) + imagePath := filepath.Join("testdata", "flux-images") + images, err := ExtractComponentImagesWithDigest(imagePath, opts) g.Expect(err).NotTo(HaveOccurred()) g.Expect(images).To(HaveLen(6)) g.Expect(images).To(ContainElements( @@ -62,7 +63,7 @@ func TestBuild_FetchImages(t *testing.T) { )) opts.Registry = "registry.local/fluxcd" - _, err = FetchComponentImages(opts) + _, err = ExtractComponentImagesWithDigest(imagePath, opts) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("unsupported registry")) } diff --git a/internal/builder/pull.go b/internal/builder/pull.go new file mode 100644 index 0000000..eade685 --- /dev/null +++ b/internal/builder/pull.go @@ -0,0 +1,48 @@ +// Copyright 2024 Stefan Prodan. +// SPDX-License-Identifier: AGPL-3.0 + +package builder + +import ( + "context" + "fmt" + "strings" + + "github.com/fluxcd/pkg/tar" + "github.com/google/go-containerregistry/pkg/crane" +) + +// PullArtifact downloads an artifact from an OCI repository and extracts the content +// of the first tgz layer to the given destination directory. +// It returns the digest of the artifact. +func PullArtifact(ctx context.Context, ociURL, dstDir string) (string, error) { + img, err := crane.Pull(strings.TrimPrefix(ociURL, "oci://"), crane.WithContext(ctx)) + if err != nil { + return "", fmt.Errorf("pulling artifact %s failed: %w", ociURL, err) + } + + digest, err := img.Digest() + if err != nil { + return "", fmt.Errorf("parsing digest for artifact %s failed: %w", ociURL, err) + } + + layers, err := img.Layers() + if err != nil { + return "", fmt.Errorf("listing layers in artifact %s failed: %w", ociURL, err) + } + + if len(layers) < 1 { + return "", fmt.Errorf("no layers found in artifact %s", ociURL) + } + + blob, err := layers[0].Compressed() + if err != nil { + return "", fmt.Errorf("extracting layer from artifact %s failed: %w", ociURL, err) + } + + if err = tar.Untar(blob, dstDir, tar.WithMaxUntarSize(-1)); err != nil { + return "", fmt.Errorf("extracting layer from artifact %s failed: %w", ociURL, err) + } + + return digest.String(), nil +} diff --git a/internal/builder/testdata/flux-images/v2.3.0/enterprise-alpine.yaml b/internal/builder/testdata/flux-images/v2.3.0/enterprise-alpine.yaml new file mode 100644 index 0000000..35ea19e --- /dev/null +++ b/internal/builder/testdata/flux-images/v2.3.0/enterprise-alpine.yaml @@ -0,0 +1,19 @@ +images: + - name: ghcr.io/controlplaneio-fluxcd/alpine/source-controller + newTag: v1.3.0 + digest: sha256:20dd417c31decab169fe1384a52e122aceb14061ff61f8bf59324b7808326ca5 + - name: ghcr.io/controlplaneio-fluxcd/alpine/kustomize-controller + newTag: v1.3.0 + digest: sha256:f86abc81aee1ed60b18ae137bec23d3f63b30b303fb926fc4b61643c63734c28 + - name: ghcr.io/controlplaneio-fluxcd/alpine/helm-controller + newTag: v1.0.1 + digest: sha256:88672f9d1f73a84c14d2dcc9373f264abc7589ec304b72b322c5dfe78643a3d9 + - name: ghcr.io/controlplaneio-fluxcd/alpine/notification-controller + newTag: v1.3.0 + digest: sha256:9894577d753297d03c288a9ae0149168a6b6cfd07e89bed3d2747c03639379ba + - name: ghcr.io/controlplaneio-fluxcd/alpine/image-reflector-controller + newTag: v0.32.0 + digest: sha256:4b80a314ef52af28809020ea2c1ae30c23ceafe65a871cb1d005b01681cd22a7 + - name: ghcr.io/controlplaneio-fluxcd/alpine/image-automation-controller + newTag: v0.38.0 + digest: sha256:64e31d9eec9c1aec025d49ff6af1a72352ebbbd6a37b5490e18153a4cbc0f709 diff --git a/internal/builder/testdata/flux-images/v2.3.0/enterprise-distroless.yaml b/internal/builder/testdata/flux-images/v2.3.0/enterprise-distroless.yaml new file mode 100644 index 0000000..5606044 --- /dev/null +++ b/internal/builder/testdata/flux-images/v2.3.0/enterprise-distroless.yaml @@ -0,0 +1,19 @@ +images: + - name: ghcr.io/controlplaneio-fluxcd/distroless/source-controller + newTag: v1.3.0 + digest: sha256:3b34a63a635779b2b3ea67ec02f5925704dc93d39efc4b92243e2170907615af + - name: ghcr.io/controlplaneio-fluxcd/distroless/kustomize-controller + newTag: v1.3.0 + digest: sha256:d9facae045d079175245f6e596e3d614c71f4b3420e3e9b486ae99cb62549d27 + - name: ghcr.io/controlplaneio-fluxcd/distroless/helm-controller + newTag: v1.0.1 + digest: sha256:54235750a3fd73836413eee0408fa7ab3b7546ed53ea5dc418780a40687af710 + - name: ghcr.io/controlplaneio-fluxcd/distroless/notification-controller + newTag: v1.3.0 + digest: sha256:c1fcf015dcb9ace4f958fdf6fa11e8c05d896ed02fff1a8b138f3285bc9121d2 + - name: ghcr.io/controlplaneio-fluxcd/distroless/image-reflector-controller + newTag: v0.32.0 + digest: sha256:5c535a22048a277f160639ff34ddafa60093a09ee344684abfc5c16d041188f5 + - name: ghcr.io/controlplaneio-fluxcd/distroless/image-automation-controller + newTag: v0.38.0 + digest: sha256:9da97e7b8b5d6d83ed0e0382387d0d7664f55673af98c80f85a0e37590991ce1 diff --git a/internal/builder/testdata/flux-images/v2.3.0/upstream-alpine.yaml b/internal/builder/testdata/flux-images/v2.3.0/upstream-alpine.yaml new file mode 100644 index 0000000..866316b --- /dev/null +++ b/internal/builder/testdata/flux-images/v2.3.0/upstream-alpine.yaml @@ -0,0 +1,19 @@ +images: + - name: ghcr.io/fluxcd/source-controller + newTag: v1.3.0 + digest: sha256:161da425b16b64dda4b3cec2ba0f8d7442973aba29bb446db3b340626181a0bc + - name: ghcr.io/fluxcd/kustomize-controller + newTag: v1.3.0 + digest: sha256:48a032574dd45c39750ba0f1488e6f1ae36756a38f40976a6b7a588d83acefc1 + - name: ghcr.io/fluxcd/helm-controller + newTag: v1.0.1 + digest: sha256:a67a037faa850220ff94d8090253732079589ad9ff10b6ddf294f3b7cd0f3424 + - name: ghcr.io/fluxcd/notification-controller + newTag: v1.3.0 + digest: sha256:c0fab940c7e578ea519097d36c040238b0cc039ce366fdb753947428bbf0c3d6 + - name: ghcr.io/fluxcd/image-reflector-controller + newTag: v0.32.0 + digest: sha256:aed795c7a8b85bca93f6d199d5a14bbefaf925ad5aa5316b32a716cfa4070d0b + - name: ghcr.io/fluxcd/image-automation-controller + newTag: v0.38.0 + digest: sha256:ab5097213194f3cd9f0e68d8a937d94c4fc7e821f6544453211e94815b282aa2 diff --git a/internal/controller/fluxinstance_controller.go b/internal/controller/fluxinstance_controller.go index e14905b..89f1a11 100644 --- a/internal/controller/fluxinstance_controller.go +++ b/internal/controller/fluxinstance_controller.go @@ -117,9 +117,35 @@ func (r *FluxInstanceReconciler) reconcile(ctx context.Context, conditions.MarkReconciling(obj, meta.ProgressingReason, msg) + if err := r.patch(ctx, obj, patcher); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update status: %w", err) + } + + tmpDir, err := builder.MkdirTempAbs("", "flux") + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create tmp dir: %w", err) + } + + defer func() { + if err := os.RemoveAll(tmpDir); err != nil { + log.Error(err, "failed to remove tmp dir", "dir", tmpDir) + } + }() + + // Fetch the distribution manifests. + manifestsDir, err := r.fetch(ctx, obj, tmpDir) + if err != nil { + msg := fmt.Sprintf("fetch failed: %s", err.Error()) + conditions.MarkFalse(obj, + meta.ReadyCondition, + meta.ArtifactFailedReason, + msg) + r.EventRecorder.Event(obj, corev1.EventTypeWarning, meta.ArtifactFailedReason, msg) + return ctrl.Result{}, err + } // Build the distribution manifests. - buildResult, err := r.build(ctx, obj) + buildResult, err := r.build(ctx, obj, manifestsDir) if err != nil { msg := fmt.Sprintf("build failed: %s", err.Error()) conditions.MarkFalse(obj, @@ -178,18 +204,55 @@ func (r *FluxInstanceReconciler) reconcile(ctx context.Context, return requeueAfter(obj), nil } -// build reads the distribution manifests from the storage path, +// fetch pulls the distribution OCI artifact and +// extracts the manifests to the temporary directory. +// If the distribution artifact URL is not provided, +// it falls back to the manifests stored in the container storage. +func (r *FluxInstanceReconciler) fetch(ctx context.Context, + obj *fluxcdv1.FluxInstance, tmpDir string) (string, error) { + log := ctrl.LoggerFrom(ctx) + artifactURL := obj.Spec.Distribution.Artifact + + // Pull the latest manifests from the OCI repository. + if artifactURL != "" { + ctxPull, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + artifactDigest, err := builder.PullArtifact(ctxPull, artifactURL, tmpDir) + if err != nil { + return "", err + } + log.Info("fetched latest manifests", "url", artifactURL, "digest", artifactDigest) + return tmpDir, nil + } + + // Fall back to the manifests stored in container storage. + return r.StoragePath, nil +} + +// build reads the distribution manifests from the local storage, // matches the version and builds the final resources. func (r *FluxInstanceReconciler) build(ctx context.Context, - obj *fluxcdv1.FluxInstance) (*builder.Result, error) { + obj *fluxcdv1.FluxInstance, manifestsDir string) (*builder.Result, error) { log := ctrl.LoggerFrom(ctx) - fluxDir := filepath.Join(r.StoragePath, "flux") - if _, err := os.Stat(fluxDir); os.IsNotExist(err) { - return nil, fmt.Errorf("storage path %s does not exist", fluxDir) + fluxManifestsDir := filepath.Join(manifestsDir, "flux") + if _, err := os.Stat(fluxManifestsDir); os.IsNotExist(err) { + return nil, fmt.Errorf("storage path %s does not exist", fluxManifestsDir) } - ver, err := builder.MatchVersion(fluxDir, obj.Spec.Distribution.Version) + tmpDir, err := builder.MkdirTempAbs("", "flux") + if err != nil { + return nil, fmt.Errorf("failed to create tmp dir: %w", err) + } + + defer func() { + if err := os.RemoveAll(tmpDir); err != nil { + log.Error(err, "failed to remove tmp dir", "dir", tmpDir) + } + }() + + ver, err := builder.MatchVersion(fluxManifestsDir, obj.Spec.Distribution.Version) if err != nil { return nil, err } @@ -242,8 +305,8 @@ func (r *FluxInstanceReconciler) build(ctx context.Context, options.Patches += string(patchesData) } - srcDir := filepath.Join(fluxDir, ver) - images, err := builder.FetchComponentImages(options) + srcDir := filepath.Join(fluxManifestsDir, ver) + images, err := builder.ExtractComponentImagesWithDigest(filepath.Join(manifestsDir, "flux-images"), options) if err != nil { log.Error(err, "falling back to extracting images from manifests") images, err = builder.ExtractComponentImages(srcDir, options) @@ -253,17 +316,6 @@ func (r *FluxInstanceReconciler) build(ctx context.Context, } options.ComponentImages = images - tmpDir, err := builder.MkdirTempAbs("", "flux") - if err != nil { - return nil, fmt.Errorf("failed to create tmp dir: %w", err) - } - - defer func() { - if err := os.RemoveAll(tmpDir); err != nil { - log.Error(err, "failed to remove tmp dir", "dir", tmpDir) - } - }() - return builder.Build(srcDir, tmpDir, options) } @@ -495,6 +547,8 @@ func requeueAfter(obj *fluxcdv1.FluxInstance) ctrl.Result { return result } +// fmtDuration returns a human-readable string +// representation of the time duration. func fmtDuration(t time.Time) string { if time.Since(t) < time.Second { return time.Since(t).Round(time.Millisecond).String() diff --git a/internal/controller/fluxinstance_controller_test.go b/internal/controller/fluxinstance_controller_test.go index 84a3685..44afb90 100644 --- a/internal/controller/fluxinstance_controller_test.go +++ b/internal/controller/fluxinstance_controller_test.go @@ -26,6 +26,7 @@ import ( func TestFluxInstanceReconciler_LifeCycle(t *testing.T) { g := NewWithT(t) + const manifestsURL = "oci://ghcr.io/controlplaneio-fluxcd/flux-operator-manifests:latest" reconciler := getFluxInstanceReconciler() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -40,6 +41,7 @@ func TestFluxInstanceReconciler_LifeCycle(t *testing.T) { }, Spec: getDefaultFluxSpec(), } + obj.Spec.Distribution.Artifact = manifestsURL // Initialize the instance. err = testEnv.Create(ctx, obj) @@ -101,6 +103,13 @@ func TestFluxInstanceReconciler_LifeCycle(t *testing.T) { g.Expect(result.Status.Components[2].Repository).To(Equal("ghcr.io/fluxcd/helm-controller")) g.Expect(result.Status.Components[3].Repository).To(Equal("ghcr.io/fluxcd/notification-controller")) + // Check if the deployments images have digests. + sc := &appsv1.Deployment{} + err = testClient.Get(ctx, types.NamespacedName{Name: "source-controller", Namespace: ns.Name}, sc) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(sc.Spec.Template.Spec.Containers[0].Image).To(HavePrefix("ghcr.io/fluxcd/source-controller")) + g.Expect(sc.Spec.Template.Spec.Containers[0].Image).To(ContainSubstring("@sha256:")) + // Update the instance. resultP := result.DeepCopy() resultP.SetAnnotations(map[string]string{ @@ -189,7 +198,69 @@ func TestFluxInstanceReconciler_LifeCycle(t *testing.T) { g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) } -func TestFluxInstanceReconciler_InstallFail(t *testing.T) { +func TestFluxInstanceReconciler_FetchFail(t *testing.T) { + g := NewWithT(t) + const manifestsURL = "oci://not.found/artifact" + reconciler := getFluxInstanceReconciler() + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + ns, err := testEnv.CreateNamespace(ctx, "test") + g.Expect(err).ToNot(HaveOccurred()) + + obj := &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: ns.Name, + Namespace: ns.Name, + }, + Spec: getDefaultFluxSpec(), + } + obj.Spec.Distribution.Artifact = manifestsURL + + err = testClient.Create(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + + // Initialize the instance. + r, err := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(r.Requeue).To(BeTrue()) + + // Try to install the instance. + r, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + g.Expect(err).To(HaveOccurred()) + + // Check if the instance was marked as failed. + result := &fluxcdv1.FluxInstance{} + err = testClient.Get(ctx, client.ObjectKeyFromObject(obj), result) + g.Expect(err).ToNot(HaveOccurred()) + + logObjectStatus(t, result) + g.Expect(conditions.IsStalled(result)).To(BeFalse()) + g.Expect(conditions.IsFalse(result, meta.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(result, meta.ReadyCondition)).To(BeIdenticalTo(meta.ArtifactFailedReason)) + g.Expect(conditions.GetMessage(result, meta.ReadyCondition)).To(ContainSubstring(manifestsURL)) + g.Expect(conditions.GetReason(result, meta.ReconcilingCondition)).To(BeIdenticalTo(meta.ProgressingWithRetryReason)) + + // Check if events were recorded for each step. + events := getEvents(result.Name) + g.Expect(events).To(HaveLen(1)) + g.Expect(events[0].Reason).To(Equal(meta.ArtifactFailedReason)) + + err = testClient.Delete(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + + r, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(r.IsZero()).To(BeTrue()) +} + +func TestFluxInstanceReconciler_BuildFail(t *testing.T) { g := NewWithT(t) reconciler := getFluxInstanceReconciler() reconciler.StoragePath = "notfound" @@ -523,7 +594,7 @@ func getDefaultFluxSpec() fluxcdv1.FluxInstanceSpec { return fluxcdv1.FluxInstanceSpec{ Wait: false, Distribution: fluxcdv1.Distribution{ - Version: "v2.3.x", + Version: "v2.3.0", Registry: "ghcr.io/fluxcd", }, Sync: &fluxcdv1.Sync{