Files
pulumi-docker-build/provider/internal/index.go
2024-04-16 23:15:43 -07:00

324 lines
8.6 KiB
Go

// 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/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)
var (
_ infer.Annotated = (*Index)(nil)
_ infer.Annotated = (*IndexArgs)(nil)
_ 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
}
ctx.Log(diag.Debug, fmt.Sprintf("creating index with tag %s and sources %s", input.Tag, input.Sources))
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 {
ctx.Log(diag.Debug, "skipping read because index was not pushed")
return name, input, state, nil // Nothing to read.
}
cli, err := i.client(ctx, state, input)
if err != nil {
return name, input, state, err
}
ctx.Log(diag.Debug, "reading index with tag "+input.Tag)
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, nil
}
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 != nil && news.Registry != nil {
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
}
}
if (olds.Registry == nil && news.Registry != nil) ||
(olds.Registry != nil && news.Registry == nil) {
diff["registry"] = 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,
_ IndexState,
args IndexArgs,
) (Client, error) {
cfg := infer.GetConfig[Config](ctx)
if cli, ok := ctx.Value(_mockClientKey).(Client); ok {
return cli, nil
}
// We prefer auth from args, the provider, and state in that order. We
// build a slice in reverse order because wrap() will overwrite earlier
// entries with later ones.
auths := []Registry{}
auths = append(auths, cfg.Registries...)
if args.Registry != nil {
auths = append(auths, *args.Registry)
}
return wrap(cfg.host, auths...)
}