Forklift buildx provider
This commit is contained in:
306
provider/internal/index.go
Normal file
306
provider/internal/index.go
Normal file
@@ -0,0 +1,306 @@
|
||||
// Copyright 2024, Pulumi Corporation.
|
||||
//
|
||||
// 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
// For examples/docs.
|
||||
_ "embed"
|
||||
|
||||
provider "github.com/pulumi/pulumi-go-provider"
|
||||
"github.com/pulumi/pulumi-go-provider/infer"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
_ infer.Annotated = (infer.Annotated)((*Index)(nil))
|
||||
_ infer.Annotated = (infer.Annotated)((*IndexArgs)(nil))
|
||||
_ infer.Annotated = (infer.Annotated)((*IndexState)(nil))
|
||||
_ infer.CustomCheck[IndexArgs] = (*Index)(nil)
|
||||
_ infer.CustomResource[IndexArgs, IndexState] = (*Index)(nil)
|
||||
_ infer.CustomDelete[IndexState] = (*Index)(nil)
|
||||
_ infer.CustomDiff[IndexArgs, IndexState] = (*Index)(nil)
|
||||
_ infer.CustomRead[IndexArgs, IndexState] = (*Index)(nil)
|
||||
_ infer.CustomUpdate[IndexArgs, IndexState] = (*Index)(nil)
|
||||
)
|
||||
|
||||
//go:embed embed/index-examples.md
|
||||
var _indexExamples string
|
||||
|
||||
// Index is an OCI index or manifest list on a remote registry.
|
||||
type Index struct{}
|
||||
|
||||
// IndexArgs instantiate an Index.
|
||||
type IndexArgs struct {
|
||||
Tag string `pulumi:"tag"`
|
||||
Sources []string `pulumi:"sources"`
|
||||
Push bool `pulumi:"push,optional"`
|
||||
Registry Registry `pulumi:"registry,optional"`
|
||||
}
|
||||
|
||||
// IndexState captures the state of an Index.
|
||||
type IndexState struct {
|
||||
IndexArgs
|
||||
|
||||
Ref string `pulumi:"ref" provider:"output"`
|
||||
}
|
||||
|
||||
// Annotate sets docstrings and defaults on Index.
|
||||
func (i *Index) Annotate(a infer.Annotator) {
|
||||
a.Describe(&i, dedent(`
|
||||
An index (or manifest list) referencing one or more existing images.
|
||||
|
||||
Useful for crafting a multi-platform image from several
|
||||
platform-specific images.
|
||||
|
||||
This creates an OCI image index or a Docker manifest list depending on
|
||||
the media types of the source images.
|
||||
`)+
|
||||
"\n\n"+_indexExamples,
|
||||
)
|
||||
}
|
||||
|
||||
// Annotate sets docstrings and defaults on IndexArgs.
|
||||
func (i *IndexArgs) Annotate(a infer.Annotator) {
|
||||
a.Describe(&i.Registry, dedent(`
|
||||
Authentication for the registry where the tagged index will be pushed.
|
||||
|
||||
Credentials can also be included with the provider's configuration.
|
||||
`))
|
||||
a.Describe(&i.Sources, dedent(`
|
||||
Existing images to include in the index.
|
||||
`))
|
||||
a.Describe(&i.Tag, dedent(`
|
||||
The tag to apply to the index.
|
||||
`))
|
||||
a.Describe(&i.Push, dedent(`
|
||||
If true, push the index to the target registry.
|
||||
|
||||
Defaults to "true".
|
||||
`))
|
||||
|
||||
a.SetDefault(&i.Push, true)
|
||||
}
|
||||
|
||||
// Annotate sets docstrings on IndexState.
|
||||
func (i *IndexState) Annotate(a infer.Annotator) {
|
||||
a.Describe(&i.Ref, dedent(`
|
||||
The pushed tag with digest.
|
||||
|
||||
Identical to the tag if the index was not pushed.
|
||||
`))
|
||||
}
|
||||
|
||||
// Create is a passthrough to Update.
|
||||
func (i *Index) Create(
|
||||
ctx provider.Context,
|
||||
name string,
|
||||
input IndexArgs,
|
||||
preview bool,
|
||||
) (string, IndexState, error) {
|
||||
state, err := i.Update(ctx, name, IndexState{}, input, preview)
|
||||
return name, state, err
|
||||
}
|
||||
|
||||
// Update performs `buildx imagetools create` to create a new OCI index /
|
||||
// manifest list.
|
||||
func (i *Index) Update(
|
||||
ctx provider.Context,
|
||||
name string,
|
||||
state IndexState,
|
||||
input IndexArgs,
|
||||
preview bool,
|
||||
) (IndexState, error) {
|
||||
state.IndexArgs = input
|
||||
state.Ref = input.Tag
|
||||
|
||||
cli, err := i.client(ctx, state, input)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
|
||||
if preview {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
err = cli.ManifestCreate(ctx, input.Push, input.Tag, input.Sources...)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("creating: %w", err)
|
||||
}
|
||||
|
||||
_, _, state, err = i.Read(ctx, name, input, state)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("reading: %w", err)
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (i *Index) Read(
|
||||
ctx provider.Context,
|
||||
name string,
|
||||
input IndexArgs,
|
||||
state IndexState,
|
||||
) (string, IndexArgs, IndexState, error) {
|
||||
state.IndexArgs = input
|
||||
state.Ref = input.Tag
|
||||
|
||||
if !input.Push {
|
||||
return name, input, state, nil // Nothing to read.
|
||||
}
|
||||
|
||||
cli, err := i.client(ctx, state, input)
|
||||
if err != nil {
|
||||
return name, input, state, err
|
||||
}
|
||||
|
||||
digest, err := cli.ManifestInspect(ctx, input.Tag)
|
||||
if err != nil && strings.Contains(err.Error(), "No such manifest:") && input.Push {
|
||||
// A remote tag was expected but isn't there -- delete the resource.
|
||||
return "", input, state, err
|
||||
}
|
||||
if err != nil && strings.Contains(err.Error(), "No such manifest:") && !input.Push {
|
||||
// Nothing was pushed, so just use the tag without digest..
|
||||
return name, input, state, err
|
||||
}
|
||||
if err != nil {
|
||||
return name, input, state, err
|
||||
}
|
||||
|
||||
if ref, ok := addDigest(input.Tag, digest); ok {
|
||||
state.Ref = ref
|
||||
}
|
||||
|
||||
return name, input, state, nil
|
||||
}
|
||||
|
||||
// Check confirms the Index's tag and source refs are all valid. This doesn't
|
||||
// fully capture input requirements -- for example buildx requires refs to all
|
||||
// exist on the same registry. This is sufficient to handle the most common
|
||||
// cases for now.
|
||||
func (i *Index) Check(
|
||||
_ provider.Context,
|
||||
_ string,
|
||||
_ resource.PropertyMap,
|
||||
news resource.PropertyMap,
|
||||
) (IndexArgs, []provider.CheckFailure, error) {
|
||||
args, failures, err := infer.DefaultCheck[IndexArgs](news)
|
||||
if err != nil {
|
||||
return args, failures, err
|
||||
}
|
||||
|
||||
if _, err := normalizeReference(args.Tag); args.Tag != "" && err != nil {
|
||||
failures = append(
|
||||
failures,
|
||||
provider.CheckFailure{
|
||||
Property: "target",
|
||||
Reason: err.Error(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for idx, s := range args.Sources {
|
||||
if _, err := normalizeReference(s); s != "" && err != nil {
|
||||
failures = append(
|
||||
failures,
|
||||
provider.CheckFailure{
|
||||
Property: fmt.Sprintf("refs[%d]", idx),
|
||||
Reason: err.Error(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return args, failures, nil
|
||||
}
|
||||
|
||||
// Delete attempts to delete the remote manifest.
|
||||
func (i *Index) Delete(ctx provider.Context, _ string, state IndexState) error {
|
||||
if !state.Push {
|
||||
return nil // Nothing to delete.
|
||||
}
|
||||
|
||||
cli, err := i.client(ctx, state, state.IndexArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cli.ManifestDelete(ctx, state.Ref)
|
||||
// TODO: Upstream buildx swallows the error types we'd like to test for
|
||||
// here.
|
||||
if err != nil && strings.Contains(err.Error(), "No such manifest:") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Diff returns a diff of proposed changes against current state. Ideally we
|
||||
// wouldn't need to implement all of this, but we currently have to in order to
|
||||
// force `ignoreChanges`-style behavior on our registry password (which can
|
||||
// change all the time due to short-lived AWS credentials).
|
||||
func (i *Index) Diff(
|
||||
_ provider.Context,
|
||||
_ string,
|
||||
olds IndexState,
|
||||
news IndexArgs,
|
||||
) (provider.DiffResponse, error) {
|
||||
diff := map[string]provider.PropertyDiff{}
|
||||
update := provider.PropertyDiff{Kind: provider.Update}
|
||||
replace := provider.PropertyDiff{Kind: provider.UpdateReplace}
|
||||
|
||||
if olds.Tag != news.Tag {
|
||||
diff["tag"] = replace
|
||||
}
|
||||
if !reflect.DeepEqual(olds.Sources, news.Sources) {
|
||||
diff["sources"] = update
|
||||
}
|
||||
if olds.Registry.Address != news.Registry.Address {
|
||||
diff["registry.address"] = update
|
||||
if olds.Registry.Address != "" {
|
||||
diff["registry.address"] = replace
|
||||
}
|
||||
}
|
||||
if olds.Registry.Username != news.Registry.Username {
|
||||
diff["registry.username"] = update
|
||||
}
|
||||
// Intentionally ignore changes to registry.password
|
||||
|
||||
return provider.DiffResponse{
|
||||
HasChanges: len(diff) > 0,
|
||||
DetailedDiff: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// client produces a CLI client with scoped to this resource and layered on top
|
||||
// of any host-level credentials.
|
||||
func (i *Index) client(
|
||||
ctx provider.Context,
|
||||
state IndexState,
|
||||
args IndexArgs,
|
||||
) (Client, error) {
|
||||
cfg := infer.GetConfig[Config](ctx)
|
||||
|
||||
if cli, ok := ctx.Value(_mockClientKey).(Client); ok {
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
auths := cfg.Registries
|
||||
auths = append(auths, state.Registry)
|
||||
auths = append(auths, args.Registry)
|
||||
|
||||
return wrap(cfg.host, auths...)
|
||||
}
|
||||
Reference in New Issue
Block a user