crossplane
219 строк · 7.0 Кб
1/*
2Copyright 2023 The Crossplane Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package xpkg
18
19import (
20"context"
21"path/filepath"
22
23"github.com/google/go-containerregistry/pkg/name"
24v1 "github.com/google/go-containerregistry/pkg/v1"
25"github.com/google/go-containerregistry/pkg/v1/daemon"
26"github.com/google/go-containerregistry/pkg/v1/tarball"
27"github.com/spf13/afero"
28metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29"k8s.io/apimachinery/pkg/runtime"
30
31"github.com/crossplane/crossplane-runtime/pkg/errors"
32"github.com/crossplane/crossplane-runtime/pkg/logging"
33"github.com/crossplane/crossplane-runtime/pkg/parser"
34
35"github.com/crossplane/crossplane/internal/xpkg"
36"github.com/crossplane/crossplane/internal/xpkg/parser/examples"
37"github.com/crossplane/crossplane/internal/xpkg/parser/yaml"
38)
39
40const (
41errGetNameFromMeta = "failed to get package name from crossplane.yaml"
42errBuildPackage = "failed to build package"
43errImageDigest = "failed to get package digest"
44errCreatePackage = "failed to create package file"
45errParseRuntimeImageRef = "failed to parse runtime image reference"
46errPullRuntimeImage = "failed to pull runtime image"
47errLoadRuntimeTarball = "failed to load runtime tarball"
48errGetRuntimeBaseImageOpts = "failed to get runtime base image options"
49)
50
51// AfterApply constructs and binds context to any subcommands
52// that have Run() methods that receive it.
53func (c *buildCmd) AfterApply() error {
54c.fs = afero.NewOsFs()
55
56root, err := filepath.Abs(c.PackageRoot)
57if err != nil {
58return err
59}
60c.root = root
61
62ex, err := filepath.Abs(c.ExamplesRoot)
63if err != nil {
64return err
65}
66
67pp, err := yaml.New()
68if err != nil {
69return err
70}
71
72c.builder = xpkg.New(
73parser.NewFsBackend(
74c.fs,
75parser.FsDir(root),
76parser.FsFilters(
77append(
78buildFilters(root, c.Ignore),
79xpkg.SkipContains(c.ExamplesRoot))...),
80),
81parser.NewFsBackend(
82c.fs,
83parser.FsDir(ex),
84parser.FsFilters(
85buildFilters(ex, c.Ignore)...),
86),
87pp,
88examples.New(),
89)
90
91return nil
92}
93
94// buildCmd builds a crossplane package.
95type buildCmd struct {
96// Flags. Keep sorted alphabetically.
97EmbedRuntimeImage string `placeholder:"NAME" help:"An OCI image to embed in the package as its runtime." xor:"runtime-image"`
98EmbedRuntimeImageTarball string `placeholder:"PATH" type:"existingfile" help:"An OCI image tarball to embed in the package as its runtime." xor:"runtime-image"`
99ExamplesRoot string `short:"e" type:"path" help:"A directory of example YAML files to include in the package." default:"./examples"`
100Ignore []string `placeholder:"PATH" help:"Comma-separated file paths, specified relative to --package-root, to exclude from the package. Wildcards are supported. Directories cannot be excluded."`
101PackageFile string `short:"o" type:"path" placeholder:"PATH" help:"The file to write the package to. Defaults to a generated filename in --package-root."`
102PackageRoot string `short:"f" type:"existingdir" help:"The directory that contains the package's crossplane.yaml file." default:"."`
103
104// Internal state. These aren't part of the user-exposed CLI structure.
105fs afero.Fs
106builder *xpkg.Builder
107root string
108}
109
110func (c *buildCmd) Help() string {
111return `
112This command builds a package file from a local directory of files.
113
114Examples:
115
116# Build a package from the files in the 'package' directory.
117crossplane xpkg build --package-root=package/
118
119# Build a package that embeds a Provider's controller OCI image built with
120# 'docker build' so that the package can also be used to run the provider.
121# Provider and Function packages support embedding runtime images.
122crossplane xpkg build --embed-runtime-image=cc873e13cdc1
123`
124}
125
126// GetRuntimeBaseImageOpts returns the controller base image options.
127func (c *buildCmd) GetRuntimeBaseImageOpts() ([]xpkg.BuildOpt, error) {
128switch {
129case c.EmbedRuntimeImageTarball != "":
130img, err := tarball.ImageFromPath(filepath.Clean(c.EmbedRuntimeImageTarball), nil)
131if err != nil {
132return nil, errors.Wrap(err, errLoadRuntimeTarball)
133}
134return []xpkg.BuildOpt{xpkg.WithBase(img)}, nil
135case c.EmbedRuntimeImage != "":
136// We intentionally don't override the default registry here. Doing so
137// leads to unintuitive behavior, in that you can't tag your runtime
138// image as some/image:latest then pass that same tag to xpkg build.
139// Instead you'd need to pass index.docker.io/some/image:latest.
140ref, err := name.ParseReference(c.EmbedRuntimeImage)
141if err != nil {
142return nil, errors.Wrap(err, errParseRuntimeImageRef)
143}
144img, err := daemon.Image(ref, daemon.WithContext(context.Background()))
145if err != nil {
146return nil, errors.Wrap(err, errPullRuntimeImage)
147}
148return []xpkg.BuildOpt{xpkg.WithBase(img)}, nil
149}
150return nil, nil
151
152}
153
154// GetOutputFileName prepares output file name.
155func (c *buildCmd) GetOutputFileName(meta runtime.Object, hash v1.Hash) (string, error) {
156output := filepath.Clean(c.PackageFile)
157if c.PackageFile == "" {
158pkgMeta, ok := meta.(metav1.Object)
159if !ok {
160return "", errors.New(errGetNameFromMeta)
161}
162pkgName := xpkg.FriendlyID(pkgMeta.GetName(), hash.Hex)
163output = xpkg.BuildPath(c.root, pkgName, xpkg.XpkgExtension)
164}
165return output, nil
166}
167
168// Run executes the build command.
169func (c *buildCmd) Run(logger logging.Logger) error {
170var buildOpts []xpkg.BuildOpt
171rtBuildOpts, err := c.GetRuntimeBaseImageOpts()
172if err != nil {
173return errors.Wrap(err, errGetRuntimeBaseImageOpts)
174}
175buildOpts = append(buildOpts, rtBuildOpts...)
176
177img, meta, err := c.builder.Build(context.Background(), buildOpts...)
178if err != nil {
179return errors.Wrap(err, errBuildPackage)
180}
181
182hash, err := img.Digest()
183if err != nil {
184return errors.Wrap(err, errImageDigest)
185}
186
187output, err := c.GetOutputFileName(meta, hash)
188if err != nil {
189return err
190}
191
192f, err := c.fs.Create(output)
193if err != nil {
194return errors.Wrap(err, errCreatePackage)
195}
196
197defer func() { _ = f.Close() }()
198if err := tarball.Write(nil, img, f); err != nil {
199return err
200}
201logger.Info("xpkg saved", "output", output)
202return nil
203}
204
205// default build filters skip directories, empty files, and files without YAML
206// extension in addition to any paths specified.
207func buildFilters(root string, skips []string) []parser.FilterFn {
208defaultFns := []parser.FilterFn{
209parser.SkipDirs(),
210parser.SkipNotYAML(),
211parser.SkipEmpty(),
212}
213opts := make([]parser.FilterFn, len(skips)+len(defaultFns))
214copy(opts, defaultFns)
215for i, s := range skips {
216opts[i+len(defaultFns)] = parser.SkipPath(filepath.Join(root, s))
217}
218return opts
219}
220