Skip to content

pass embedded cluster artifacts in upstream upgrade #4549

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

Merged
merged 9 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion cmd/kots/cli/admin-console-push-images.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func AdminPushImagesCmd() *cobra.Command {
}

if _, err := os.Stat(imageSource); err == nil {
_, err = image.TagAndPushImagesFromBundle(imageSource, *options)
err = image.TagAndPushImagesFromBundle(imageSource, *options)
if err != nil {
return errors.Wrap(err, "failed to push images")
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/replicatedhq/embedded-cluster-kinds v1.1.2
github.com/replicatedhq/kotskinds v0.0.0-20240326213823-6a0ed11e7397
github.com/replicatedhq/kotskinds v0.0.0-20240415152931-2837245679f5
github.com/replicatedhq/kurlkinds v1.5.0
github.com/replicatedhq/troubleshoot v0.87.0
github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,10 @@ github.com/replicatedhq/embedded-cluster-kinds v1.1.2 h1:2ITzcUzh5uh0fsnfZsVHvkw
github.com/replicatedhq/embedded-cluster-kinds v1.1.2/go.mod h1:LheSDOgMngMRAbwAj0sVZUVv2ciKIVR2bYTMeOBGwlg=
github.com/replicatedhq/kotskinds v0.0.0-20240326213823-6a0ed11e7397 h1:JNuBcFH9D3Osyi+1QUwdvaAklEd6HXznqZDfpWlr73M=
github.com/replicatedhq/kotskinds v0.0.0-20240326213823-6a0ed11e7397/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I=
github.com/replicatedhq/kotskinds v0.0.0-20240402213802-a6d75e97be70 h1:55fMr60YysSf+ac5zeW+xTIIJ8edUpAjorQyFn0iP2c=
github.com/replicatedhq/kotskinds v0.0.0-20240402213802-a6d75e97be70/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I=
github.com/replicatedhq/kotskinds v0.0.0-20240415152931-2837245679f5 h1:SI1Ar5c6QWTm1IpPOO/CVv8dktdqd8KaQUEOeHEbhgM=
github.com/replicatedhq/kotskinds v0.0.0-20240415152931-2837245679f5/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I=
github.com/replicatedhq/kurlkinds v1.5.0 h1:zZ0PKNeh4kXvSzVGkn62DKTo314GxhXg1TSB3azURMc=
github.com/replicatedhq/kurlkinds v1.5.0/go.mod h1:rUpBMdC81IhmJNCWMU/uRsMETv9P0xFoMvdSP/TAr5A=
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851 h1:eRlNDHxGfVkPCRXbA4BfQJvt5DHjFiTtWy3R/t4djyY=
Expand Down
20 changes: 20 additions & 0 deletions pkg/embeddedcluster/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"sort"
"time"

embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1"
"github.com/replicatedhq/kots/pkg/imageutil"
"github.com/replicatedhq/kots/pkg/k8sutil"
"github.com/replicatedhq/kots/pkg/logger"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
Expand Down Expand Up @@ -171,3 +173,21 @@ func startClusterUpgrade(ctx context.Context, newcfg embeddedclusterv1beta1.Conf
}
return nil
}

type EmbeddedClusterArtifactOCIPathOptions struct {
RegistryHost string
RegistryNamespace string
ChannelID string
UpdateCursor string
VersionLabel string
}

// EmbeddedClusterArtifactOCIPath returns the OCI path for an embedded cluster artifact given
// the artifact filename and details about the configured registry and channel release.
func EmbeddedClusterArtifactOCIPath(filename string, opts EmbeddedClusterArtifactOCIPathOptions) string {
name := filepath.Base(filename)
repository := filepath.Join("embedded-cluster", imageutil.SanitizeRepo(name))
tag := imageutil.SanitizeTag(fmt.Sprintf("%s-%s-%s", opts.ChannelID, opts.UpdateCursor, opts.VersionLabel))
artifact := fmt.Sprintf("%s:%s", filepath.Join(opts.RegistryHost, opts.RegistryNamespace, repository), tag)
return artifact
}
104 changes: 104 additions & 0 deletions pkg/embeddedcluster/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,107 @@ func Test_getArtifactsFromInstallation(t *testing.T) {
})
}
}

func TestEmbeddedClusterArtifactOCIPath(t *testing.T) {
type args struct {
filename string
opts EmbeddedClusterArtifactOCIPathOptions
}
tests := []struct {
name string
args args
want string
}{
{
name: "happy path for binary",
args: args{
filename: "embedded-cluster/embedded-cluster-amd64",
opts: EmbeddedClusterArtifactOCIPathOptions{
RegistryHost: "registry.example.com",
RegistryNamespace: "my-app",
ChannelID: "test-channel-id",
UpdateCursor: "1",
VersionLabel: "1.0.0",
},
},
want: "registry.example.com/my-app/embedded-cluster/embedded-cluster-amd64:test-channel-id-1-1.0.0",
},
{
name: "happy path for charts bundle",
args: args{
filename: "embedded-cluster/charts.tar.gz",
opts: EmbeddedClusterArtifactOCIPathOptions{
RegistryHost: "registry.example.com",
RegistryNamespace: "my-app",
ChannelID: "test-channel-id",
UpdateCursor: "1",
VersionLabel: "1.0.0",
},
},
want: "registry.example.com/my-app/embedded-cluster/charts.tar.gz:test-channel-id-1-1.0.0",
},
{
name: "happy path for image bundle",
args: args{
filename: "embedded-cluster/images-amd64.tar",
opts: EmbeddedClusterArtifactOCIPathOptions{
RegistryHost: "registry.example.com",
RegistryNamespace: "my-app",
ChannelID: "test-channel-id",
UpdateCursor: "1",
VersionLabel: "1.0.0",
},
},
want: "registry.example.com/my-app/embedded-cluster/images-amd64.tar:test-channel-id-1-1.0.0",
},
{
name: "happy path for version metadata",
args: args{
filename: "embedded-cluster/version-metadata.json",
opts: EmbeddedClusterArtifactOCIPathOptions{
RegistryHost: "registry.example.com",
RegistryNamespace: "my-app",
ChannelID: "test-channel-id",
UpdateCursor: "1",
VersionLabel: "1.0.0",
},
},
want: "registry.example.com/my-app/embedded-cluster/version-metadata.json:test-channel-id-1-1.0.0",
},
{
name: "file with name that needs to be sanitized",
args: args{
filename: "A file with spaces.tar.gz",
opts: EmbeddedClusterArtifactOCIPathOptions{
RegistryHost: "registry.example.com",
RegistryNamespace: "my-app",
ChannelID: "test-channel-id",
UpdateCursor: "1",
VersionLabel: "1.0.0",
},
},
want: "registry.example.com/my-app/embedded-cluster/afilewithspaces.tar.gz:test-channel-id-1-1.0.0",
},
{
name: "version label name that needs to be sanitized",
args: args{
filename: "test.txt",
opts: EmbeddedClusterArtifactOCIPathOptions{
RegistryHost: "registry.example.com",
RegistryNamespace: "my-app",
ChannelID: "test-channel-id",
UpdateCursor: "1",
VersionLabel: "A version with spaces",
},
},
want: "registry.example.com/my-app/embedded-cluster/test.txt:test-channel-id-1-Aversionwithspaces",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := EmbeddedClusterArtifactOCIPath(tt.args.filename, tt.args.opts); got != tt.want {
t.Errorf("EmbeddedClusterArtifactOCIPath() = %v, want %v", got, tt.want)
}
})
}
}
36 changes: 15 additions & 21 deletions pkg/image/airgap.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ func WriteProgressLine(progressWriter io.Writer, line string) {
}

// CopyAirgapImages pushes images found in the airgap bundle/airgap root to the configured registry.
func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger) (*imagetypes.CopyAirgapImagesResult, error) {
func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger) error {
if opts.AirgapBundle == "" {
return &imagetypes.CopyAirgapImagesResult{}, nil
return nil
}

pushOpts := imagetypes.PushImagesOptions{
Expand All @@ -125,27 +125,25 @@ func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger
LogForUI: true,
}

copyResult, err := TagAndPushImagesFromBundle(opts.AirgapBundle, pushOpts)
err := TagAndPushImagesFromBundle(opts.AirgapBundle, pushOpts)
if err != nil {
return nil, errors.Wrap(err, "failed to push images from bundle")
return errors.Wrap(err, "failed to push images from bundle")
}

return &imagetypes.CopyAirgapImagesResult{
EmbeddedClusterArtifacts: copyResult.EmbeddedClusterArtifacts,
}, nil
return nil
}

func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImagesOptions) (*imagetypes.CopyAirgapImagesResult, error) {
func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImagesOptions) error {
airgap, err := kotsutil.FindAirgapMetaInBundle(airgapBundle)
if err != nil {
return nil, errors.Wrap(err, "failed to find airgap meta")
return errors.Wrap(err, "failed to find airgap meta")
}

switch airgap.Spec.Format {
case dockertypes.FormatDockerRegistry:
extractedBundle, err := os.MkdirTemp("", "extracted-airgap-kots")
if err != nil {
return nil, errors.Wrap(err, "failed to create temp dir for unarchived airgap bundle")
return errors.Wrap(err, "failed to create temp dir for unarchived airgap bundle")
}
defer os.RemoveAll(extractedBundle)

Expand All @@ -155,34 +153,30 @@ func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImag
},
}
if err := tarGz.Unarchive(airgapBundle, extractedBundle); err != nil {
return nil, errors.Wrap(err, "falied to unarchive airgap bundle")
return errors.Wrap(err, "falied to unarchive airgap bundle")
}
if err := PushImagesFromTempRegistry(extractedBundle, airgap.Spec.SavedImages, options); err != nil {
return nil, errors.Wrap(err, "failed to push images from docker registry bundle")
return errors.Wrap(err, "failed to push images from docker registry bundle")
}
case dockertypes.FormatDockerArchive, "":
if err := PushImagesFromDockerArchiveBundle(airgapBundle, options); err != nil {
return nil, errors.Wrap(err, "failed to push images from docker archive bundle")
return errors.Wrap(err, "failed to push images from docker archive bundle")
}
default:
return nil, errors.Errorf("Airgap bundle format '%s' is not supported", airgap.Spec.Format)
return errors.Errorf("Airgap bundle format '%s' is not supported", airgap.Spec.Format)
}

pushEmbeddedArtifactsOpts := imagetypes.PushEmbeddedClusterArtifactsOptions{
Registry: options.Registry,
Tag: imageutil.SanitizeTag(fmt.Sprintf("%s-%s-%s", airgap.Spec.ChannelID, airgap.Spec.UpdateCursor, airgap.Spec.VersionLabel)),
HTTPClient: orasretry.DefaultClient,
}
pushedArtifacts, err := PushEmbeddedClusterArtifacts(airgapBundle, pushEmbeddedArtifactsOpts)
_, err = PushEmbeddedClusterArtifacts(airgapBundle, pushEmbeddedArtifactsOpts)
if err != nil {
return nil, errors.Wrap(err, "failed to push embedded cluster artifacts")
return errors.Wrap(err, "failed to push embedded cluster artifacts")
}

result := &imagetypes.CopyAirgapImagesResult{
EmbeddedClusterArtifacts: pushedArtifacts,
}

return result, nil
return nil
}

func PushImagesFromTempRegistry(airgapRootDir string, imageList []string, options imagetypes.PushImagesOptions) error {
Expand Down
2 changes: 1 addition & 1 deletion pkg/kotsadm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func Deploy(deployOptions types.DeployOptions, log *logger.CLILogger) error {
}

if !deployOptions.DisableImagePush {
_, err := image.TagAndPushImagesFromBundle(deployOptions.AirgapBundle, pushOptions)
err := image.TagAndPushImagesFromBundle(deployOptions.AirgapBundle, pushOptions)
if err != nil {
return errors.Wrap(err, "failed to tag and push app images from path")
}
Expand Down
10 changes: 1 addition & 9 deletions pkg/midstream/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,10 @@ func WriteMidstream(opts WriteOptions) (*Midstream, error) {
io.WriteString(opts.ProcessImageOptions.ReportWriter, "Copying images\n")

if opts.ProcessImageOptions.IsAirgap {
copyResult, err := image.CopyAirgapImages(opts.ProcessImageOptions, opts.Log)
err := image.CopyAirgapImages(opts.ProcessImageOptions, opts.Log)
if err != nil {
return nil, errors.Wrap(err, "failed to copy airgap images")
}

if err := image.UpdateInstallationEmbeddedClusterArtifacts(image.UpdateInstallationEmbeddedClusterArtifactsOptions{
Artifacts: copyResult.EmbeddedClusterArtifacts,
KotsKinds: opts.KotsKinds,
UpstreamDir: opts.UpstreamDir,
}); err != nil {
return nil, errors.Wrap(err, "failed to update installation airgap artifacts")
}
Comment on lines -119 to -126
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need to write back to the installation.yaml anymore with push results since we're just using what is in the airgap metadata.

} else {
err := image.CopyOnlineImages(opts.ProcessImageOptions, allImages, opts.KotsKinds, opts.License, dockerHubRegistryCreds, opts.Log)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion pkg/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
fetchOptions.CurrentVersionIsRequired = installation.Spec.IsRequired
fetchOptions.CurrentReplicatedRegistryDomain = installation.Spec.ReplicatedRegistryDomain
fetchOptions.CurrentReplicatedProxyDomain = installation.Spec.ReplicatedProxyDomain
fetchOptions.CurrentEmbeddedClusterArtifacts = installation.Spec.EmbeddedClusterArtifacts
}

if pullOptions.AirgapRoot != "" {
Expand Down Expand Up @@ -364,7 +365,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
}
if processImageOptions.RewriteImages && processImageOptions.IsAirgap {
// if this is an airgap install, we still need to process the images
if _, err := image.CopyAirgapImages(processImageOptions, log); err != nil {
if err := image.CopyAirgapImages(processImageOptions, log); err != nil {
return "", errors.Wrap(err, "failed to copy airgap images")
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/rewrite/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func Rewrite(rewriteOptions RewriteOptions) error {
CurrentReplicatedRegistryDomain: rewriteOptions.Installation.Spec.ReplicatedRegistryDomain,
CurrentReplicatedProxyDomain: rewriteOptions.Installation.Spec.ReplicatedProxyDomain,
CurrentReplicatedChartNames: rewriteOptions.Installation.Spec.ReplicatedChartNames,
CurrentEmbeddedClusterArtifacts: rewriteOptions.Installation.Spec.EmbeddedClusterArtifacts,
EncryptionKey: rewriteOptions.Installation.Spec.EncryptionKey,
License: rewriteOptions.License,
AppSequence: rewriteOptions.AppSequence,
Expand Down
11 changes: 11 additions & 0 deletions pkg/tests/pull/cases/airgap/upstream/userdata/installation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: kots.io/v1beta1
kind: Installation
metadata:
creationTimestamp: null
name: my-app
spec:
channelID: 1vusIYZLAVxMG6q760OJmRKj5i5
channelName: My Channel
embeddedClusterArtifacts:
- ttl.sh/replicated/charts.tar.gz:v1
status: {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ metadata:
spec:
channelID: 1vusIYZLAVxMG6q760OJmRKj5i5
channelName: My Channel
embeddedClusterArtifacts:
- ttl.sh/replicated/charts.tar.gz:v1
knownImages:
- image: alpine
isPrivate: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ metadata:
spec:
channelID: 1vusIYZLAVxMG6q760OJmRKj5i5
channelName: My Channel
embeddedClusterArtifacts:
- ttl.sh/replicated/charts.tar.gz:v1
knownImages:
- image: alpine
isPrivate: true
Expand Down
3 changes: 3 additions & 0 deletions pkg/tests/pull/pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ func TestKotsPull(t *testing.T) {
require.ElementsMatch(t, wantInstallation.Spec.KnownImages, installation.Spec.KnownImages)
wantInstallation.Spec.KnownImages = nil
installation.Spec.KnownImages = nil
require.ElementsMatch(t, wantInstallation.Spec.EmbeddedClusterArtifacts, installation.Spec.EmbeddedClusterArtifacts)
wantInstallation.Spec.EmbeddedClusterArtifacts = nil
installation.Spec.EmbeddedClusterArtifacts = nil
require.Equal(t, wantInstallation, installation)
})
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/upstream/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/pkg/errors"
"github.com/replicatedhq/kots/pkg/crypto"
"github.com/replicatedhq/kots/pkg/embeddedcluster"
"github.com/replicatedhq/kots/pkg/replicatedapp"
"github.com/replicatedhq/kots/pkg/upstream/types"
"github.com/replicatedhq/kots/pkg/util"
Expand Down Expand Up @@ -50,6 +51,7 @@ func downloadUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (*ty
pickReplicatedRegistryDomain(fetchOptions),
pickReplicatedProxyDomain(fetchOptions),
pickReplicatedChartNames(fetchOptions),
pickEmbeddedClusterArtifacts(fetchOptions),
fetchOptions.AppSlug,
fetchOptions.AppSequence,
fetchOptions.Airgap != nil,
Expand Down Expand Up @@ -118,3 +120,26 @@ func pickReplicatedChartNames(fetchOptions *types.FetchOptions) []string {
}
return fetchOptions.CurrentReplicatedChartNames
}

func pickEmbeddedClusterArtifacts(fetchOptions *types.FetchOptions) []string {
if fetchOptions.Airgap != nil {
if fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts == nil {
return nil
}

opts := embeddedcluster.EmbeddedClusterArtifactOCIPathOptions{
RegistryHost: fetchOptions.LocalRegistry.Hostname,
RegistryNamespace: fetchOptions.LocalRegistry.Namespace,
ChannelID: fetchOptions.Airgap.Spec.ChannelID,
UpdateCursor: fetchOptions.Airgap.Spec.UpdateCursor,
VersionLabel: fetchOptions.Airgap.Spec.VersionLabel,
}
return []string{
embeddedcluster.EmbeddedClusterArtifactOCIPath(fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts.BinaryAmd64, opts),
embeddedcluster.EmbeddedClusterArtifactOCIPath(fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts.Charts, opts),
embeddedcluster.EmbeddedClusterArtifactOCIPath(fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts.ImagesAmd64, opts),
embeddedcluster.EmbeddedClusterArtifactOCIPath(fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts.Metadata, opts),
}
}
return fetchOptions.CurrentEmbeddedClusterArtifacts
}
Loading