crossplane
258 строк · 7.4 Кб
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 xpkg18
19import (20"bytes"21"context"22"io"23"os"24"strings"25
26v1 "github.com/google/go-containerregistry/pkg/v1"27"github.com/google/go-containerregistry/pkg/v1/empty"28"github.com/google/go-containerregistry/pkg/v1/mutate"29"k8s.io/apimachinery/pkg/runtime"30"k8s.io/apimachinery/pkg/runtime/serializer/json"31
32"github.com/crossplane/crossplane-runtime/pkg/errors"33"github.com/crossplane/crossplane-runtime/pkg/parser"34
35pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"36"github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"37"github.com/crossplane/crossplane/internal/xpkg/parser/examples"38)
39
40const (41errParserPackage = "failed to parse package"42errParserExample = "failed to parse examples"43errLintPackage = "failed to lint package"44errInitBackend = "failed to initialize package parsing backend"45errTarFromStream = "failed to build tarball from stream"46errLayerFromTar = "failed to convert tarball to image layer"47errDigestInvalid = "failed to get digest from image layer"48errBuildImage = "failed to build image from layers"49errConfigFile = "failed to get config file from image"50errMutateConfig = "failed to mutate config for image"51errBuildObjectScheme = "failed to build scheme for package encoder"52)
53
54// annotatedTeeReadCloser is a copy of io.TeeReader that implements
55// parser.AnnotatedReadCloser. It returns a Reader that writes to w what it
56// reads from r. All reads from r performed through it are matched with
57// corresponding writes to w. There is no internal buffering - the write must
58// complete before the read completes. Any error encountered while writing is
59// reported as a read error. If the underlying reader is a
60// parser.AnnotatedReadCloser the tee reader will invoke its Annotate function.
61// Otherwise it will return nil. Closing is always a no-op.
62func annotatedTeeReadCloser(r io.Reader, w io.Writer) *teeReader {63return &teeReader{r, w}64}
65
66type teeReader struct {67r io.Reader68w io.Writer69}
70
71func (t *teeReader) Read(p []byte) (n int, err error) {72n, err = t.r.Read(p)73if n > 0 {74if n, err := t.w.Write(p[:n]); err != nil {75return n, err76}77}78return79}
80
81func (t *teeReader) Close() error {82return nil83}
84
85func (t *teeReader) Annotate() any {86anno, ok := t.r.(parser.AnnotatedReadCloser)87if !ok {88return nil89}90return anno.Annotate()91}
92
93// Builder defines an xpkg Builder.
94type Builder struct {95packageSource parser.Backend96exampleSource parser.Backend97
98packageParser parser.Parser99examplesParser *examples.Parser100}
101
102// New returns a new Builder.
103func New(packageSource, exampleSource parser.Backend, packageParser parser.Parser, examplesParser *examples.Parser) *Builder {104return &Builder{105packageSource: packageSource,106exampleSource: exampleSource,107packageParser: packageParser,108examplesParser: examplesParser,109}110}
111
112type buildOpts struct {113base v1.Image114}
115
116// A BuildOpt modifies how a package is built.
117type BuildOpt func(*buildOpts)118
119// WithBase sets the base image of the package.
120func WithBase(img v1.Image) BuildOpt {121return func(o *buildOpts) {122o.base = img123}124}
125
126// Build compiles a Crossplane package from an on-disk package.
127func (b *Builder) Build(ctx context.Context, opts ...BuildOpt) (v1.Image, runtime.Object, error) { //nolint:gocyclo // TODO(lsviben) consider refactoring128bOpts := &buildOpts{129base: empty.Image,130}131for _, o := range opts {132o(bOpts)133}134
135// assume examples exist136examplesExist := true137// Get package YAML stream.138pkgReader, err := b.packageSource.Init(ctx)139if err != nil {140return nil, nil, errors.Wrap(err, errInitBackend)141}142defer func() { _ = pkgReader.Close() }()143
144// Get examples YAML stream.145exReader, err := b.exampleSource.Init(ctx)146if err != nil && !os.IsNotExist(err) {147return nil, nil, errors.Wrap(err, errInitBackend)148}149defer func() { _ = exReader.Close() }()150// examples/ doesn't exist151if os.IsNotExist(err) {152examplesExist = false153}154
155pkg, err := b.packageParser.Parse(ctx, pkgReader)156if err != nil {157return nil, nil, errors.Wrap(err, errParserPackage)158}159
160metas := pkg.GetMeta()161if len(metas) != 1 {162return nil, nil, errors.New(errNotExactlyOneMeta)163}164
165// TODO(hasheddan): make linter selection logic configurable.166meta := metas[0]167var linter parser.Linter168switch meta.GetObjectKind().GroupVersionKind().Kind {169case pkgmetav1.ConfigurationKind:170linter = NewConfigurationLinter()171case v1beta1.FunctionKind:172linter = NewFunctionLinter()173case pkgmetav1.ProviderKind:174linter = NewProviderLinter()175}176if err := linter.Lint(pkg); err != nil {177return nil, nil, errors.Wrap(err, errLintPackage)178}179
180layers := make([]v1.Layer, 0)181cfgFile, err := bOpts.base.ConfigFile()182if err != nil {183return nil, nil, errors.Wrap(err, errConfigFile)184}185
186cfg := cfgFile.Config187cfg.Labels = make(map[string]string)188
189pkgBytes, err := encode(pkg)190if err != nil {191return nil, nil, errors.Wrap(err, errConfigFile)192}193
194pkgLayer, err := Layer(pkgBytes, StreamFile, PackageAnnotation, int64(pkgBytes.Len()), StreamFileMode, &cfg)195if err != nil {196return nil, nil, err197}198layers = append(layers, pkgLayer)199
200// examples exist, create the layer201if examplesExist {202exBuf := new(bytes.Buffer)203if _, err = b.examplesParser.Parse(ctx, annotatedTeeReadCloser(exReader, exBuf)); err != nil {204return nil, nil, errors.Wrap(err, errParserExample)205}206
207exLayer, err := Layer(exBuf, XpkgExamplesFile, ExamplesAnnotation, int64(exBuf.Len()), StreamFileMode, &cfg)208if err != nil {209return nil, nil, err210}211layers = append(layers, exLayer)212}213
214for _, l := range layers {215bOpts.base, err = mutate.AppendLayers(bOpts.base, l)216if err != nil {217return nil, nil, errors.Wrap(err, errBuildImage)218}219}220
221bOpts.base, err = mutate.Config(bOpts.base, cfg)222if err != nil {223return nil, nil, errors.Wrap(err, errMutateConfig)224}225
226return bOpts.base, meta, nil227}
228
229// encode encodes a package as a YAML stream. Does not check meta existence
230// or quantity i.e. it should be linted first to ensure that it is valid.
231func encode(pkg parser.Lintable) (*bytes.Buffer, error) {232pkgBuf := new(bytes.Buffer)233objScheme, err := BuildObjectScheme()234if err != nil {235return nil, errors.New(errBuildObjectScheme)236}237
238do := json.NewSerializerWithOptions(json.DefaultMetaFactory, objScheme, objScheme, json.SerializerOptions{Yaml: true})239pkgBuf.WriteString("---\n")240if err = do.Encode(pkg.GetMeta()[0], pkgBuf); err != nil {241return nil, errors.Wrap(err, errBuildObjectScheme)242}243pkgBuf.WriteString("---\n")244for _, o := range pkg.GetObjects() {245if err = do.Encode(o, pkgBuf); err != nil {246return nil, errors.Wrap(err, errBuildObjectScheme)247}248pkgBuf.WriteString("---\n")249}250return pkgBuf, nil251}
252
253// SkipContains supplies a FilterFn that skips paths that contain the give pattern.
254func SkipContains(pattern string) parser.FilterFn {255return func(path string, _ os.FileInfo) (bool, error) {256return strings.Contains(path, pattern), nil257}258}
259