-
Notifications
You must be signed in to change notification settings - Fork 382
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
Introduce k0s airgap bundle-artifacts #5360
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
/* | ||
Copyright 2024 k0s authors | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package airgap | ||
|
||
import ( | ||
"bufio" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/signal" | ||
"strconv" | ||
"syscall" | ||
|
||
"github.com/k0sproject/k0s/internal/pkg/file" | ||
"github.com/k0sproject/k0s/pkg/airgap" | ||
|
||
"k8s.io/kubectl/pkg/util/term" | ||
|
||
"github.com/containerd/platforms" | ||
"github.com/distribution/reference" | ||
imagespecv1 "github.com/opencontainers/image-spec/specs-go/v1" | ||
"github.com/sirupsen/logrus" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func newAirgapBundleArtifactsCmd(log logrus.FieldLogger, rewriteBundleRef airgap.RewriteRefFunc) *cobra.Command { | ||
var ( | ||
outPath string | ||
platform = platforms.DefaultSpec() | ||
bundler = airgap.OCIArtifactsBundler{ | ||
Log: log, | ||
RewriteTarget: rewriteBundleRef, | ||
} | ||
) | ||
|
||
cmd := &cobra.Command{ | ||
Use: "bundle-artifacts [flags] [names...]", | ||
Short: "Bundles artifacts needed for airgapped installations into a tarball", | ||
Long: `Bundles artifacts needed for airgapped installations into a tarball. Fetches the | ||
artifacts from their OCI registries and bundles them into an OCI Image Layout | ||
archive (written to standard output by default). Reads names from standard input | ||
if no names are given on the command line.`, | ||
RunE: func(cmd *cobra.Command, args []string) (err error) { | ||
ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) | ||
defer cancel() | ||
|
||
cmd.SilenceUsage = true | ||
|
||
bundler.PlatformMatcher = platforms.Only(platform) | ||
|
||
var out io.Writer | ||
if outPath == "" { | ||
out = cmd.OutOrStdout() | ||
if term.IsTerminal(out) { | ||
return errors.New("cowardly refusing to write binary data to a terminal") | ||
} | ||
} else { | ||
f, openErr := file.AtomicWithTarget(outPath).Open() | ||
if openErr != nil { | ||
return openErr | ||
} | ||
defer func() { | ||
if err == nil { | ||
err = f.Finish() | ||
} else if closeErr := f.Close(); closeErr != nil { | ||
err = errors.Join(err, closeErr) | ||
} | ||
}() | ||
out = f | ||
} | ||
|
||
var refs []reference.Named | ||
if len(args) > 0 { | ||
refs, err = parseArtifactRefs(args) | ||
} else { | ||
refs, err = parseArtifactRefsFromReader(cmd.InOrStdin()) | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
|
||
buffered := bufio.NewWriter(out) | ||
if err := bundler.Run(ctx, refs, out); err != nil { | ||
return err | ||
kke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return buffered.Flush() | ||
}, | ||
} | ||
|
||
flags := cmd.Flags() | ||
flags.StringVarP(&outPath, "output", "o", "", "output file path (writes to standard output if omitted)") | ||
flags.Var((*insecureRegistryFlag)(&bundler.InsecureRegistries), "insecure-registries", "one of no, skip-tls-verify or plain-http") | ||
flags.Var((*platformFlag)(&platform), "platform", "the platform to export") | ||
flags.StringArrayVar(&bundler.RegistriesConfigPaths, "registries-config", nil, "paths to the authentication files for OCI registries (uses the standard Docker config if omitted)") | ||
|
||
return cmd | ||
} | ||
|
||
func parseArtifactRefsFromReader(in io.Reader) ([]reference.Named, error) { | ||
words := bufio.NewScanner(in) | ||
words.Split(bufio.ScanWords) | ||
|
||
var refs []string | ||
for words.Scan() { | ||
refs = append(refs, words.Text()) | ||
} | ||
if err := words.Err(); err != nil { | ||
return nil, err | ||
} | ||
|
||
return parseArtifactRefs(refs) | ||
} | ||
|
||
func parseArtifactRefs(refs []string) ([]reference.Named, error) { | ||
var collected []reference.Named | ||
for _, ref := range refs { | ||
parsed, err := reference.ParseNormalizedNamed(ref) | ||
if err != nil { | ||
return nil, fmt.Errorf("while parsing %s: %w", ref, err) | ||
} | ||
collected = append(collected, parsed) | ||
} | ||
return collected, nil | ||
} | ||
|
||
type insecureRegistryFlag airgap.InsecureOCIRegistryKind | ||
|
||
func (insecureRegistryFlag) Type() string { | ||
return "string" | ||
} | ||
|
||
func (i insecureRegistryFlag) String() string { | ||
switch (airgap.InsecureOCIRegistryKind)(i) { | ||
case airgap.NoInsecureOCIRegistry: | ||
return "no" | ||
case airgap.SkipTLSVerifyOCIRegistry: | ||
return "skip-tls-verify" | ||
case airgap.PlainHTTPOCIRegistry: | ||
return "plain-http" | ||
default: | ||
return strconv.Itoa(int(i)) | ||
} | ||
} | ||
|
||
func (i *insecureRegistryFlag) Set(value string) error { | ||
var kind airgap.InsecureOCIRegistryKind | ||
|
||
switch value { | ||
case "no": | ||
kind = airgap.NoInsecureOCIRegistry | ||
case "skip-tls-verify": | ||
kind = airgap.SkipTLSVerifyOCIRegistry | ||
case "plain-http": | ||
kind = airgap.PlainHTTPOCIRegistry | ||
default: | ||
return errors.New("must be one of no, skip-tls-verify or plain-http") | ||
} | ||
|
||
*(*airgap.InsecureOCIRegistryKind)(i) = kind | ||
return nil | ||
} | ||
|
||
type platformFlag imagespecv1.Platform | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, stupid question, what is this for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a newtype over the OCI image spec's |
||
|
||
func (p *platformFlag) Type() string { | ||
return "string" | ||
} | ||
|
||
func (p *platformFlag) String() string { | ||
return platforms.FormatAll(*(*imagespecv1.Platform)(p)) | ||
} | ||
|
||
func (p *platformFlag) Set(value string) error { | ||
platform, err := platforms.Parse(value) | ||
if err != nil { | ||
return err | ||
} | ||
*(*imagespecv1.Platform)(p) = platform | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to run just
k0s airgap bundle-artifacts -v -o image-bundle.tar
without piping for the default images?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the idea having the input is for the users to be able to add their own images too. But that said, it could work also in the way that if the list is not given, then it could use the default images. But would that need config loading etc.?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's what I mean, if the input list is not provided, use default images. If I remember correctly, existing k0s airgap list-images reads config already, so the user could use pipes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that was kinda the idea: Not to add any config loading here at all and use pipes. Otherwise this would be duplicating quite a bit of the logic in
list-images
. This is also advertised in the long help text ofk0s airgap
: