Skip to content
Open
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
48 changes: 48 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ Version: v0.6.0
License: Apache-2.0
License URL: https://github.com/distribution/reference/blob/v0.6.0/LICENSE

----------
Module: github.com/docker/cli/cli/config
Version: v29.5.3
License: Apache-2.0
License URL: https://github.com/docker/cli/blob/v29.5.3/LICENSE

----------
Module: github.com/docker/docker-credential-helpers
Version: v0.9.8
License: MIT
License URL: https://github.com/docker/docker-credential-helpers/blob/v0.9.8/LICENSE

----------
Module: github.com/dylibso/observe-sdk/go
Version: v0.0.0-20240828172851-9145d8ad07e1
Expand Down Expand Up @@ -507,6 +519,12 @@ Version: v0.7.0
License: BSD-3-Clause
License URL: https://github.com/google/go-cmp/blob/v0.7.0/LICENSE

----------
Module: github.com/google/go-containerregistry
Version: v0.21.6
License: Apache-2.0
License URL: https://github.com/google/go-containerregistry/blob/v0.21.6/LICENSE

----------
Module: github.com/google/go-github/v74/github
Version: v74.0.0
Expand Down Expand Up @@ -681,6 +699,36 @@ Version: v1.7.7
License: Apache-2.0
License URL: https://github.com/k8snetworkplumbingwg/network-attachment-definition-client/blob/v1.7.7/LICENSE

----------
Module: github.com/klauspost/compress
Version: v1.18.6
License: MIT
License URL: https://github.com/klauspost/compress/blob/v1.18.6/LICENSE

----------
Module: github.com/klauspost/compress
Version: v1.18.6
License: Apache-2.0
License URL: https://github.com/klauspost/compress/blob/v1.18.6/LICENSE

----------
Module: github.com/klauspost/compress
Version: v1.18.6
License: BSD-3-Clause
License URL: https://github.com/klauspost/compress/blob/v1.18.6/LICENSE

----------
Module: github.com/klauspost/compress/internal/snapref
Version: v1.18.6
License: BSD-3-Clause
License URL: https://github.com/klauspost/compress/blob/v1.18.6/internal/snapref/LICENSE

----------
Module: github.com/klauspost/compress/zstd/internal/xxhash
Version: v1.18.6
License: MIT
License URL: https://github.com/klauspost/compress/blob/v1.18.6/zstd/internal/xxhash/LICENSE.txt

----------
Module: github.com/kr/fs
Version: v0.1.0
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/install_k0s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ var _ = Describe("InstallK0sCmd", func() {
c.FileWriter = mockFileWriter

mockEnv.EXPECT().GetOmsWorkdir().Return(tempDir)
mockFileWriter.EXPECT().MkdirAll(tempDir, os.FileMode(0755)).Return(nil)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unrelated fix that was necessary to run the tests locally.Seems they aren't executed in the CI

mockPM.EXPECT().ExtractDependency("kubernetes/files/k0s", false).Return(nil)
mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s")
mockK0sctl.EXPECT().Download("", false, false).Return("/tmp/k0sctl", nil)
Expand Down
28 changes: 28 additions & 0 deletions cli/cmd/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"github.com/codesphere-cloud/cs-go/pkg/io"
"github.com/spf13/cobra"
)

// LoadCmd represents the load command.
type LoadCmd struct {
cmd *cobra.Command
}

func AddLoadCmd(rootCmd *cobra.Command, opts *GlobalOptions) {
load := LoadCmd{
cmd: &cobra.Command{
Use: "load",
Short: "Load resources into a local or custom registry",
Long: io.Long(`Load resources from external sources into a local or custom registry,
e.g. mirror images from GHCR.`),
},
}

AddCmd(rootCmd, load.cmd)
AddLoadImagesCmd(load.cmd, opts)
}
112 changes: 112 additions & 0 deletions cli/cmd/load_images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"context"
"fmt"

packageio "github.com/codesphere-cloud/cs-go/pkg/io"
"github.com/codesphere-cloud/oms/internal/env"
"github.com/codesphere-cloud/oms/internal/installer"
"github.com/codesphere-cloud/oms/internal/installer/bom"
"github.com/codesphere-cloud/oms/internal/registry"
"github.com/spf13/cobra"
)

const packageBomJSON = "bom.json"

type LoadImagesCmd struct {
cmd *cobra.Command
Opts *LoadImagesOpts
Copier registry.ImageCopier
Env env.Env
}

type LoadImagesOpts struct {
*GlobalOptions
DryRun bool
Force bool
}

func AddLoadImagesCmd(load *cobra.Command, opts *GlobalOptions) {
c := &LoadImagesCmd{
cmd: &cobra.Command{
Use: "images <package> <target-registry>",
Short: "Mirror all Codesphere OCI images required to install package from Codesphere's registry",
Long: packageio.Long(`Mirror all Codesphere OCI images required to install package from Codesphere's registry into a target registry.
This is required for installations that require a custom registry, such as air-gapped environments.

Ensure that the target registry is reachable and that you have permission to push images to it.
Registry authentication is read from local container registry credentials, such as Docker config,
Docker credential helpers, or Podman auth files.

Logging in to the source and target registry before running this command is required.

To use the custom registry, it must be configured using Codesphere's configuration file before installing Codesphere.`),
Example: formatExamples("load images", []packageio.Example{
{
Cmd: "codesphere-v1.68.0.tar.gz registry.internal.example.com",
Desc: "Mirror every Codesphere OCI image required for Codesphere 1.68.0 into the target registry",
},
}),
Args: cobra.ExactArgs(2),
},
Opts: &LoadImagesOpts{
GlobalOptions: opts,
},
Env: env.NewEnv(),
}

c.cmd.Flags().BoolVar(&c.Opts.DryRun, "dry-run", false, "Print planned copy operations without copying images")
c.cmd.Flags().BoolVarP(&c.Opts.Force, "force", "f", false, "Force new package extraction even if already extracted")

AddCmd(load, c.cmd)
c.cmd.RunE = c.RunE
}

func (c *LoadImagesCmd) RunE(cmd *cobra.Command, args []string) error {
pm := installer.NewPackage(c.Env.GetOmsWorkdir(), args[0])
return c.LoadImagesFromPackage(cmd.Context(), pm, args[1])
}

func (c *LoadImagesCmd) LoadImagesFromPackage(ctx context.Context, pm installer.PackageManager, targetRegistry string) error {
bomPath, err := c.extractPackageBom(pm)
if err != nil {
return err
}

return c.LoadImagesFromBomPath(ctx, bomPath, targetRegistry)
}

func (c *LoadImagesCmd) LoadImagesFromBomPath(ctx context.Context, bomPath string, targetRegistry string) error {
config, err := bom.Parse(bomPath)
if err != nil {
return err
}

copier := c.Copier
if !c.Opts.DryRun && copier == nil {
copier = registry.NewRegistryImageCopier(ctx)
}

mirror := registry.Mirror{
Copier: copier,
DryRun: c.Opts.DryRun,
}
_, err = mirror.MirrorImages(config.ImageReferencesForCodesphereRegistry(), targetRegistry)
if err != nil {
return fmt.Errorf("failed to load images from %s: %w", bomPath, err)
}

return nil
}

func (c *LoadImagesCmd) extractPackageBom(pm installer.PackageManager) (string, error) {
if err := pm.ExtractDependency(packageBomJSON, c.Opts.Force); err != nil {
return "", fmt.Errorf("failed to extract %s from package: %w", packageBomJSON, err)
}

return pm.GetDependencyPath(packageBomJSON), nil
}
165 changes: 165 additions & 0 deletions cli/cmd/load_images_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd_test

import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"errors"
"os"
"path/filepath"

"github.com/codesphere-cloud/oms/cli/cmd"
"github.com/codesphere-cloud/oms/internal/installer"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

type fakeImageCopier struct {
calls [][2]string
err error
}

func (r *fakeImageCopier) Copy(sourceRef string, destinationRef string) error {
r.calls = append(r.calls, [2]string{sourceRef, destinationRef})
return r.err
}

var _ = Describe("LoadImages", func() {
var (
tempDir string
newPackage func() installer.PackageManager
)

BeforeEach(func() {
tempDir = GinkgoT().TempDir()
packagePath := filepath.Join(tempDir, "codesphere-test.tar.gz")
Expect(createLoadImagesTestPackage(packagePath, "bom.json", `{
"components": {
"clusterPki": {
"files": {
"chart": {
"ociRef": "ghcr.io/codesphere-cloud/charts/cluster-pki:0.1.6"
}
},
"containerImages": {
"cronjob": "ghcr.io/codesphere-cloud/docker/alpine/kubectl:1.34.2"
}
}
}
}`)).To(Succeed())

newPackage = func() installer.PackageManager {
return installer.NewPackage(filepath.Join(tempDir, "oms-workdir"), packagePath)
}
})

It("copies refs with fake copier", func() {
copier := &fakeImageCopier{}
c := &cmd.LoadImagesCmd{
Opts: &cmd.LoadImagesOpts{},
Copier: copier,
}

err := c.LoadImagesFromPackage(context.TODO(), newPackage(), "registry.internal.example.com")
Expect(err).NotTo(HaveOccurred())
Expect(copier.calls).To(Equal([][2]string{
{
"ghcr.io/codesphere-cloud/charts/cluster-pki:0.1.6",
"registry.internal.example.com/codesphere-cloud/charts/cluster-pki:0.1.6",
},
{
"ghcr.io/codesphere-cloud/docker/alpine/kubectl:1.34.2",
"registry.internal.example.com/codesphere-cloud/docker/alpine/kubectl:1.34.2",
},
}))
})

It("does not call the copier during dry-run", func() {
copier := &fakeImageCopier{}
c := &cmd.LoadImagesCmd{
Opts: &cmd.LoadImagesOpts{DryRun: true},
Copier: copier,
}

err := c.LoadImagesFromPackage(context.TODO(), newPackage(), "registry.internal.example.com")
Expect(err).NotTo(HaveOccurred())
Expect(copier.calls).To(BeEmpty())
})

It("propagates copy errors", func() {
copier := &fakeImageCopier{err: errors.New("copy failed")}
c := &cmd.LoadImagesCmd{
Opts: &cmd.LoadImagesOpts{},
Copier: copier,
}

err := c.LoadImagesFromPackage(context.TODO(), newPackage(), "registry.internal.example.com")
Expect(err).To(MatchError(ContainSubstring("copy failed")))
})
})

func createLoadImagesTestPackage(filename string, bomName string, bomContent string) error {
depsContent, err := createLoadImagesDepsArchive(map[string]string{
bomName: bomContent,
})
if err != nil {
return err
}

file, err := os.Create(filename)
if err != nil {
return err
}
defer func() { _ = file.Close() }()

gzw := gzip.NewWriter(file)
defer func() { _ = gzw.Close() }()

tw := tar.NewWriter(gzw)
defer func() { _ = tw.Close() }()

header := &tar.Header{
Name: "deps.tar.gz",
Mode: 0o600,
Size: int64(len(depsContent)),
}
if err := tw.WriteHeader(header); err != nil {
return err
}
_, err = tw.Write(depsContent)
return err
}

func createLoadImagesDepsArchive(files map[string]string) ([]byte, error) {
var buf bytes.Buffer
gzw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gzw)

for name, content := range files {
header := &tar.Header{
Name: name,
Mode: 0o600,
Size: int64(len(content)),
}
if err := tw.WriteHeader(header); err != nil {
return nil, err
}
if _, err := tw.Write([]byte(content)); err != nil {
return nil, err
}
}

if err := tw.Close(); err != nil {
return nil, err
}
if err := gzw.Close(); err != nil {
return nil, err
}

return buf.Bytes(), nil
}
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func GetRootCmd() *cobra.Command {
// Package commands
AddListCmd(rootCmd, opts)
AddDownloadCmd(rootCmd, opts)
AddLoadCmd(rootCmd, opts)
AddInstallCmd(rootCmd, opts)
AddInitCmd(rootCmd, opts)
AddTemplateCmd(rootCmd, opts)
Expand Down
Loading
Loading