Initial provider implementation (#18)

This brings over the initial buildx prototype from pulumi/pulumi-docker
and fixes various build and release issues.
This commit is contained in:
Bryce Lampe
2024-04-25 11:03:59 -07:00
committed by GitHub
parent 2545dd3089
commit 26c144c916
398 changed files with 65361 additions and 1702 deletions

31
provider/internal/auth.go Normal file
View File

@@ -0,0 +1,31 @@
// 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 "github.com/pulumi/pulumi-go-provider/infer"
// Registry contains credentials for authenticating with a remote registry.
type Registry struct {
Address string `pulumi:"address"`
Password string `pulumi:"password,optional" provider:"secret"`
Username string `pulumi:"username,optional"`
}
// Annotate sets docstrings on Registry.
func (r *Registry) Annotate(a infer.Annotator) {
a.Describe(&r.Address, `The registry's address (e.g. "docker.io").`)
a.Describe(&r.Username, `Username for the registry.`)
a.Describe(&r.Password, `Password or token for the registry.`)
}

View File

@@ -0,0 +1,38 @@
// 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 (
"github.com/pulumi/pulumi-go-provider/infer"
)
var _ infer.Annotated = (*BuilderConfig)(nil)
// BuilderConfig configures the builder to use for an image build.
type BuilderConfig struct {
Name string `pulumi:"name,optional"`
}
// Annotate sets docstrings on BuilderConfig.
func (b *BuilderConfig) Annotate(a infer.Annotator) {
a.Describe(&b.Name, dedent(`
Name of an existing buildx builder to use.
Only "docker-container", "kubernetes", or "remote" drivers are
supported. The legacy "docker" driver is not supported.
Equivalent to Docker's "--builder" flag.
`))
}

746
provider/internal/cache.go Normal file
View File

@@ -0,0 +1,746 @@
// 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 (
"errors"
"fmt"
"strings"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/util/buildflags"
"github.com/pulumi/pulumi-go-provider/infer"
)
var (
_ fmt.Stringer = (*CacheFrom)(nil)
_ fmt.Stringer = (*CacheFromAzureBlob)(nil)
_ fmt.Stringer = (*CacheFromGitHubActions)(nil)
_ fmt.Stringer = (*CacheFromLocal)(nil)
_ fmt.Stringer = (*CacheFromRegistry)(nil)
_ fmt.Stringer = (*CacheFromS3)(nil)
_ fmt.Stringer = (*CacheTo)(nil)
_ fmt.Stringer = (*CacheToAzureBlob)(nil)
_ fmt.Stringer = (*CacheToGitHubActions)(nil)
_ fmt.Stringer = (*CacheToInline)(nil)
_ fmt.Stringer = (*CacheToLocal)(nil)
_ fmt.Stringer = (*CacheToRegistry)(nil)
_ fmt.Stringer = (*CacheToS3)(nil)
_ fmt.Stringer = CacheWithCompression{}
_ fmt.Stringer = CacheWithIgnoreError{}
_ fmt.Stringer = CacheWithMode{}
_ fmt.Stringer = CacheWithOCI{}
_ infer.Annotated = (*CacheFrom)(nil)
_ infer.Annotated = (*CacheFromAzureBlob)(nil)
_ infer.Annotated = (*CacheFromGitHubActions)(nil)
_ infer.Annotated = (*CacheFromLocal)(nil)
_ infer.Annotated = (*CacheFromRegistry)(nil)
_ infer.Annotated = (*CacheFromS3)(nil)
_ infer.Annotated = (*CacheTo)(nil)
_ infer.Annotated = (*CacheToInline)(nil)
_ infer.Annotated = (*CacheToLocal)(nil)
_ infer.Annotated = (*CacheWithCompression)(nil)
_ infer.Annotated = (*CacheWithIgnoreError)(nil)
_ infer.Annotated = (*CacheWithMode)(nil)
_ infer.Annotated = (*CacheWithOCI)(nil)
_ infer.Enum[CacheMode] = (*CacheMode)(nil)
_ infer.Enum[CompressionType] = (*CompressionType)(nil)
)
// CacheFromLocal pulls cache manifests from a local directory.
type CacheFromLocal struct {
Src string `pulumi:"src"`
Digest string `pulumi:"digest,optional"`
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c *CacheFromLocal) String() string {
if c == nil {
return ""
}
parts := []string{"type=local"}
if c.Src != "" {
parts = append(parts, "src="+c.Src)
}
if c.Digest != "" {
parts = append(parts, "digest="+c.Digest)
}
return strings.Join(parts, ",")
}
// Annotate sets docstrings on CacheFromLocal.
func (c *CacheFromLocal) Annotate(a infer.Annotator) {
a.Describe(&c.Src, "Path of the local directory where cache gets imported from.")
a.Describe(&c.Digest, "Digest of manifest to import.")
}
// CacheFromRegistry pulls cache manifests from a registry ref.
type CacheFromRegistry struct {
Ref string `pulumi:"ref"`
}
// Annotate sets docstrings on CacheFromRegistry.
func (c *CacheFromRegistry) Annotate(a infer.Annotator) {
a.Describe(&c.Ref, "Fully qualified name of the cache image to import.")
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c *CacheFromRegistry) String() string {
if c == nil {
return ""
}
return "type=registry,ref=" + c.Ref
}
// CacheWithOCI exposes OCI media type options.
type CacheWithOCI struct {
OCI *bool `pulumi:"ociMediaTypes,optional"`
ImageManifest *bool `pulumi:"imageManifest,optional"`
}
// Annotate sets docstrings on CacheWithOCI.
func (c *CacheWithOCI) Annotate(a infer.Annotator) {
a.Describe(&c.OCI, dedent(`
Whether to use OCI media types in exported manifests. Defaults to
"true".
`))
a.Describe(&c.ImageManifest, dedent(`
Export cache manifest as an OCI-compatible image manifest instead of a
manifest list. Requires "ociMediaTypes" to also be "true".
Some registries like AWS ECR will not work with caching if this is
"false".
Defaults to "false" to match Docker's default behavior.
`))
a.SetDefault(&c.OCI, true)
a.SetDefault(&c.ImageManifest, false)
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if unknown.
func (c CacheWithOCI) String() string {
if c.OCI == nil {
return ""
}
parts := []string{fmt.Sprintf("oci-mediatypes=%t", *c.OCI)}
if c.ImageManifest != nil {
parts = append(parts, fmt.Sprintf("image-manifest=%t", *c.ImageManifest))
}
return strings.Join(parts, ",")
}
// CacheFromGitHubActions pulls cache manifests from the GitHub actions cache.
type CacheFromGitHubActions struct {
URL string `pulumi:"url,optional"`
Token string `pulumi:"token,optional" provider:"secret"`
Scope string `pulumi:"scope,optional"`
}
// Annotate sets docstrings on CacheFromGitHubActions.
func (c *CacheFromGitHubActions) Annotate(a infer.Annotator) {
a.SetDefault(&c.URL, "", "ACTIONS_RUNTIME_URL")
a.SetDefault(&c.Token, "", "ACTIONS_RUNTIME_TOKEN")
a.SetDefault(&c.Scope, "", "buildkit")
a.Describe(&c.URL, dedent(`
The cache server URL to use for artifacts.
Defaults to "$ACTIONS_RUNTIME_URL", although a separate action like
"crazy-max/ghaction-github-runtime" is recommended to expose this
environment variable to your jobs.
`))
a.Describe(&c.Token, dedent(`
The GitHub Actions token to use. This is not a personal access tokens
and is typically generated automatically as part of each job.
Defaults to "$ACTIONS_RUNTIME_TOKEN", although a separate action like
"crazy-max/ghaction-github-runtime" is recommended to expose this
environment variable to your jobs.
`))
a.Describe(&c.Scope, dedent(`
The scope to use for cache keys. Defaults to "buildkit".
This should be set if building and caching multiple images in one
workflow, otherwise caches will overwrite each other.
`))
}
func (c *CacheFromGitHubActions) String() string {
if c == nil {
return ""
}
parts := []string{"type=gha"}
if c.Scope != "" {
parts = append(parts, "scope="+c.Scope)
}
if c.Token != "" {
parts = append(parts, "token="+c.Token)
}
if c.URL != "" {
parts = append(parts, "url="+c.URL)
}
return strings.Join(parts, ",")
}
// CacheFromAzureBlob pulls cache manifests from Azure
// blob storage.
type CacheFromAzureBlob struct {
Name string `pulumi:"name"`
AccountURL string `pulumi:"accountUrl,optional"`
SecretAccessKey string `pulumi:"secretAccessKey,optional" provider:"secret"`
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c *CacheFromAzureBlob) String() string {
if c == nil {
return ""
}
parts := []string{"type=azblob"}
if c.Name != "" {
parts = append(parts, "name="+c.Name)
}
if c.AccountURL != "" {
parts = append(parts, "account_url="+c.AccountURL)
}
if c.SecretAccessKey != "" {
parts = append(parts, "secret_access_key="+c.SecretAccessKey)
}
return strings.Join(parts, ",")
}
// Annotate sets docstrings on CacheFromAzureBlob.
func (c *CacheFromAzureBlob) Annotate(a infer.Annotator) {
a.Describe(&c.Name, "The name of the cache image.")
a.Describe(&c.AccountURL, "Base URL of the storage account.")
a.Describe(&c.SecretAccessKey, "Blob storage account key.")
}
// CacheToAzureBlob pushes cache manifests to Azure blob storage.
type CacheToAzureBlob struct {
CacheWithMode
CacheWithIgnoreError
CacheFromAzureBlob
}
func (c *CacheToAzureBlob) String() string {
if c == nil {
return ""
}
return join(&c.CacheFromAzureBlob, c.CacheWithMode, c.CacheWithIgnoreError)
}
// CacheFromS3 pulls cache manifests from S3-compatible APIs.
type CacheFromS3 struct {
Region string `pulumi:"region"`
Bucket string `pulumi:"bucket"`
Name string `pulumi:"name,optional"`
EndpointURL string `pulumi:"endpointUrl,optional"`
BlobsPrefix string `pulumi:"blobsPrefix,optional"`
ManifestsPrefix string `pulumi:"manifestsPrefix,optional"`
UsePathStyle *bool `pulumi:"usePathStyle,optional"`
AccessKeyID string `pulumi:"accessKeyId,optional"`
SecretAccessKey string `pulumi:"secretAccessKey,optional" provider:"secret"`
SessionToken string `pulumi:"sessionToken,optional" provider:"secret"`
}
// Annotate sets docstrings and defaults on CacheFromS3.
func (c *CacheFromS3) Annotate(a infer.Annotator) {
a.SetDefault(&c.Region, "", "AWS_REGION")
a.SetDefault(&c.AccessKeyID, "", "AWS_ACCESS_KEY_ID")
a.SetDefault(&c.SecretAccessKey, "", "AWS_SECRET_ACCESS_KEY")
a.SetDefault(&c.SessionToken, "", "AWS_SESSION_TOKEN")
a.Describe(&c.Bucket, dedent(`
Name of the S3 bucket.
`))
a.Describe(&c.Region, dedent(`
The geographic location of the bucket. Defaults to "$AWS_REGION".
`))
a.Describe(&c.AccessKeyID, dedent(`
Defaults to "$AWS_ACCESS_KEY_ID".
`))
a.Describe(&c.SecretAccessKey, dedent(`
Defaults to "$AWS_SECRET_ACCESS_KEY".
`))
a.Describe(&c.SessionToken, dedent(`
Defaults to "$AWS_SESSION_TOKEN".
`))
a.Describe(&c.BlobsPrefix, dedent(`
Prefix to prepend to blob filenames.
`))
a.Describe(&c.EndpointURL, dedent(`
Endpoint of the S3 bucket.
`))
a.Describe(&c.ManifestsPrefix, dedent(`
Prefix to prepend on manifest filenames.
`))
a.Describe(&c.Name, dedent(`
Name of the cache image.
`))
a.Describe(&c.UsePathStyle, dedent(`
Uses "bucket" in the URL instead of hostname when "true".
`))
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c *CacheFromS3) String() string {
if c == nil {
return ""
}
parts := []string{"type=s3"}
if c.Bucket != "" {
parts = append(parts, "bucket="+c.Bucket)
}
if c.Name != "" {
parts = append(parts, "name="+c.Name)
}
if c.EndpointURL != "" {
parts = append(parts, "endpoint_url="+c.EndpointURL)
}
if c.BlobsPrefix != "" {
parts = append(parts, "blobs_prefix="+c.BlobsPrefix)
}
if c.ManifestsPrefix != "" {
parts = append(parts, "manifests_prefix="+c.ManifestsPrefix)
}
if c.UsePathStyle != nil {
parts = append(parts, fmt.Sprintf("use_path_type=%t", *c.UsePathStyle))
}
if c.AccessKeyID != "" {
parts = append(parts, "access_key_id="+c.AccessKeyID)
}
if c.SecretAccessKey != "" {
parts = append(parts, "secret_access_key="+c.SecretAccessKey)
}
if c.SessionToken != "" {
parts = append(parts, "session_token="+c.SessionToken)
}
return strings.Join(parts, ",")
}
// CacheWithMode is a cache that can configure its mode.
type CacheWithMode struct {
Mode *CacheMode `pulumi:"mode,optional"`
}
// Annotate sets docstrings and defaults on CacheWithMode.
func (c *CacheWithMode) Annotate(a infer.Annotator) {
m := Min
a.SetDefault(&c.Mode, &m)
a.Describe(&c.Mode, dedent(`
The cache mode to use. Defaults to "min".
`))
}
func (c CacheWithMode) String() string {
if c.Mode == nil {
return ""
}
return fmt.Sprintf("mode=%s", *c.Mode)
}
// CacheWithIgnoreError exposes an option to ignore errors during caching.
type CacheWithIgnoreError struct {
IgnoreError *bool `pulumi:"ignoreError,optional"`
}
// Annotate sets docstrings and defaults on CacheWithIgnoreError.
func (c *CacheWithIgnoreError) Annotate(a infer.Annotator) {
a.SetDefault(&c.IgnoreError, false)
a.Describe(&c.IgnoreError, "Ignore errors caused by failed cache exports.")
}
func (c CacheWithIgnoreError) String() string {
if c.IgnoreError == nil {
return ""
}
return fmt.Sprintf("ignore-error=%t", *c.IgnoreError)
}
// CacheToS3 pushes cache manifests to an S3-compatible API.
type CacheToS3 struct {
CacheWithMode
CacheWithIgnoreError
CacheFromS3
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c *CacheToS3) String() string {
if c == nil {
return ""
}
return join(&c.CacheFromS3, c.CacheWithMode, c.CacheWithIgnoreError)
}
// Raw is a CLI-encoded cache entry appropriate for passing directly to the
// CLI. Useful if the Docker backend supports cache types not captured by our
// API, or if the user just prefers "type=..." inputs.
type Raw string
// String return the raw string as-is. This can be empty during previews where
// the user has provided a value but it is unknown.
func (c Raw) String() string {
return string(c)
}
// CacheFrom is a "union" type for all of our available `--cache-from` options.
type CacheFrom struct {
Local *CacheFromLocal `pulumi:"local,optional"`
Registry *CacheFromRegistry `pulumi:"registry,optional"`
GHA *CacheFromGitHubActions `pulumi:"gha,optional"`
AZBlob *CacheFromAzureBlob `pulumi:"azblob,optional"`
S3 *CacheFromS3 `pulumi:"s3,optional"`
Raw Raw `pulumi:"raw,optional"`
Disabled bool `pulumi:"disabled,optional"`
}
// Annotate sets docstrings and defaults on CacheFrom.
func (c *CacheFrom) Annotate(a infer.Annotator) {
a.Describe(&c.Local, dedent(`
A simple backend which caches images on your local filesystem.
`))
a.Describe(&c.Registry, dedent(`
Upload build caches to remote registries.
`))
a.Describe(&c.GHA, dedent(`
Recommended for use with GitHub Actions workflows.
An action like "crazy-max/ghaction-github-runtime" is recommended to
expose appropriate credentials to your GitHub workflow.
`))
a.Describe(&c.AZBlob, dedent(`
Upload build caches to Azure's blob storage service.
`))
a.Describe(&c.S3, dedent(`
Upload build caches to AWS S3 or an S3-compatible services such as
MinIO.
`))
a.Describe(&c.Raw, dedent(`
A raw string as you would provide it to the Docker CLI (e.g.,
"type=inline").
`))
a.Describe(&c.Disabled, dedent(`
When "true" this entry will be excluded. Defaults to "false".
`))
}
// String returns a CLI-encoded value for this `--cache-from` entry, or an
// empty string if disabled. `validate` should be called to ensure only one
// entry was set.
func (c CacheFrom) String() string {
if c.Disabled {
return ""
}
return join(c.Local, c.Registry, c.GHA, c.AZBlob, c.S3, c.Raw)
}
func (c CacheFrom) validate(preview bool) (*controllerapi.CacheOptionsEntry, error) {
if strings.Count(c.String(), "type=") > 1 {
return nil, errors.New("cacheFrom should only specify one cache type")
}
parsed, err := buildflags.ParseCacheEntry([]string{c.String()})
if err != nil {
return nil, err
}
if len(parsed) == 0 {
// This can happen for example if we have a GHA cache but no GitHub
// environment variables set.
// Shouldn't happen...
return nil, nil
}
return parsed[0], nil
}
// CacheToInline embeds cache information directly into an image.
type CacheToInline struct{}
// String returns the CLI-encoded value of these cache options, or an empty
// string if unknown.
func (c *CacheToInline) String() string {
if c == nil {
return ""
}
return "type=inline"
}
// Annotate sets docstrings on CacheToInline.
func (c *CacheToInline) Annotate(a infer.Annotator) {
a.Describe(&c, dedent(`
Include an inline cache with the exported image.
`))
}
// CacheToLocal writes cache manifests to a local directory.
type CacheToLocal struct {
CacheWithCompression
CacheWithIgnoreError
CacheWithMode
Dest string `pulumi:"dest"`
}
// Annotate sets docstrings on CacheToLocal.
func (c *CacheToLocal) Annotate(a infer.Annotator) {
a.Describe(&c.Dest, dedent(`
Path of the local directory to export the cache.
`))
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c *CacheToLocal) String() string {
if c == nil {
return ""
}
return join(
Raw("type=local,dest="+c.Dest),
c.CacheWithCompression,
c.CacheWithIgnoreError,
)
}
// CacheToRegistry pushes cache manifests to a remote registry.
type CacheToRegistry struct {
CacheWithMode
CacheWithIgnoreError
CacheWithOCI
CacheWithCompression
CacheFromRegistry
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c *CacheToRegistry) String() string {
if c == nil {
return ""
}
return join(
&c.CacheFromRegistry,
c.CacheWithMode,
c.CacheWithIgnoreError,
c.CacheWithOCI,
c.CacheWithCompression,
)
}
// CacheWithCompression is a cache with options to configure compression
// settings.
type CacheWithCompression struct {
Compression *CompressionType `pulumi:"compression,optional"`
CompressionLevel int `pulumi:"compressionLevel,optional"`
ForceCompression *bool `pulumi:"forceCompression,optional"`
}
// Annotate sets docstrings and defaults on CacheWithCompression.
func (c *CacheWithCompression) Annotate(a infer.Annotator) {
gz := Gzip
a.SetDefault(&c.Compression, &gz)
a.SetDefault(&c.CompressionLevel, 0)
a.SetDefault(&c.ForceCompression, false)
a.Describe(&c.Compression, "The compression type to use.")
a.Describe(&c.CompressionLevel, "Compression level from 0 to 22.")
a.Describe(&c.ForceCompression, "Forcefully apply compression.")
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c CacheWithCompression) String() string {
if c.CompressionLevel == 0 {
return ""
}
parts := []string{}
if c.Compression != nil {
parts = append(parts, fmt.Sprintf("compression=%s", *c.Compression))
}
if c.CompressionLevel > 0 {
cl := c.CompressionLevel
if cl > 22 {
cl = 22
}
parts = append(parts, fmt.Sprintf("compression-level=%d", cl))
}
if c.ForceCompression != nil {
parts = append(parts, fmt.Sprintf("force-compression=%t", *c.ForceCompression))
}
return strings.Join(parts, ",")
}
// CacheToGitHubActions pushes cache manifests to the GitHub Actions cache
// backend.
type CacheToGitHubActions struct {
CacheWithMode
CacheWithIgnoreError
CacheFromGitHubActions
}
// String returns the CLI-encoded value of these cache options, or an empty
// string if the receiver is nil.
func (c *CacheToGitHubActions) String() string {
if c == nil {
return ""
}
return join(&c.CacheFromGitHubActions, c.CacheWithMode, c.CacheWithIgnoreError)
}
// CacheTo is a "union" type for all of our available `--cache-to` options.
type CacheTo struct {
Inline *CacheToInline `pulumi:"inline,optional"`
Local *CacheToLocal `pulumi:"local,optional"`
Registry *CacheToRegistry `pulumi:"registry,optional"`
GHA *CacheToGitHubActions `pulumi:"gha,optional"`
AZBlob *CacheToAzureBlob `pulumi:"azblob,optional"`
S3 *CacheToS3 `pulumi:"s3,optional"`
Raw Raw `pulumi:"raw,optional"`
Disabled bool `pulumi:"disabled,optional"`
}
// Annotate sets docstrings and defaults on CacheTo.
func (c *CacheTo) Annotate(a infer.Annotator) {
a.Describe(&c.Inline, dedent(`
The inline cache storage backend is the simplest implementation to get
started with, but it does not handle multi-stage builds. Consider the
"registry" cache backend instead.
`))
a.Describe(&c.Local, dedent(`
A simple backend which caches imagines on your local filesystem.
`))
a.Describe(&c.Registry, dedent(`
Push caches to remote registries. Incompatible with the "docker" build
driver.
`))
a.Describe(&c.GHA, dedent(`
Recommended for use with GitHub Actions workflows.
An action like "crazy-max/ghaction-github-runtime" is recommended to
expose appropriate credentials to your GitHub workflow.
`))
a.Describe(&c.AZBlob, dedent(`
Push cache to Azure's blob storage service.
`))
a.Describe(&c.S3, dedent(`
Push cache to AWS S3 or S3-compatible services such as MinIO.
`))
a.Describe(&c.Raw, dedent(`
A raw string as you would provide it to the Docker CLI (e.g.,
"type=inline")`,
))
a.Describe(&c.Disabled, dedent(`
When "true" this entry will be excluded. Defaults to "false".
`))
}
// String returns a CLI-encoded value for this `--cache-to` entry, or an
// empty string if disabled. `validate` should be called to ensure only one
// entry was set.
func (c CacheTo) String() string {
if c.Disabled {
return ""
}
return join(c.Inline, c.Local, c.Registry, c.GHA, c.AZBlob, c.S3, c.Raw)
}
func (c CacheTo) validate(preview bool) (*controllerapi.CacheOptionsEntry, error) {
if strings.Count(c.String(), "type=") > 1 {
return nil, errors.New("cacheTo should only specify one cache type")
}
parsed, err := buildflags.ParseCacheEntry([]string{c.String()})
if err != nil {
return nil, err
}
if len(parsed) == 0 {
// This can happen for example if we have a GHA cache but no GitHub
// environment variables set.
// Shouldn't happen...
return nil, nil
}
return parsed[0], nil
}
// CacheMode controls the complexity of exported cache manifests.
type CacheMode string
const (
Min CacheMode = "min" // Min cache mode.
Max CacheMode = "max" // Max cache mode.
)
// Values returns all valid CacheMode values for SDK generation.
func (CacheMode) Values() []infer.EnumValue[CacheMode] {
return []infer.EnumValue[CacheMode]{
{
Value: Min,
Description: "Only layers that are exported into the resulting image are cached.",
},
{
Value: Max,
Description: "All layers are cached, even those of intermediate steps.",
},
}
}
// CompressionType is the algorithm used for compressing blobs.
type CompressionType string
const (
Gzip CompressionType = "gzip" // Gzip compression.
Estargz CompressionType = "estargz" // Estargz compression.
Zstd CompressionType = "zstd" // Zstd compression.
)
// Values returns all valid CompressionType values for SDK generation.
func (CompressionType) Values() []infer.EnumValue[CompressionType] {
return []infer.EnumValue[CompressionType]{
{Value: Gzip, Description: "Use `gzip` for compression."},
{Value: Estargz, Description: "Use `estargz` for compression."},
{Value: Zstd, Description: "Use `zstd` for compression."},
}
}
type joiner struct{ sep string }
func (j joiner) join(ss ...fmt.Stringer) string {
parts := []string{}
for _, s := range ss {
p := s.String()
if p == "" {
continue
}
parts = append(parts, p)
}
return strings.Join(parts, j.sep)
}
func join(ss ...fmt.Stringer) string {
return joiner{","}.join(ss...)
}

View File

@@ -0,0 +1,139 @@
// 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"
"testing"
"github.com/docker/buildx/util/buildflags"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func TestCacheString(t *testing.T) {
t.Parallel()
gzip := Gzip
tests := []struct {
name string
given fmt.Stringer
want string
}{
{
name: "s3",
given: CacheTo{S3: &CacheToS3{
CacheFromS3: CacheFromS3{
Region: "us-west-2",
Bucket: "bucket-foo",
Name: "myname",
EndpointURL: "https://some.endpoint",
BlobsPrefix: "blob-prefix",
ManifestsPrefix: "manifest-prefix",
UsePathStyle: pulumi.BoolRef(true),
AccessKeyID: "access-key-id",
SecretAccessKey: "secret-key",
SessionToken: "session",
},
}},
//nolint:lll // Taken from AWS reference docs.
want: "type=s3,bucket=bucket-foo,name=myname,endpoint_url=https://some.endpoint,blobs_prefix=blob-prefix,manifests_prefix=manifest-prefix,use_path_type=true,access_key_id=access-key-id,secret_access_key=secret-key,session_token=session",
},
{
name: "gha",
given: CacheTo{GHA: &CacheToGitHubActions{}},
want: "type=gha",
},
{
name: "from-local",
given: CacheFrom{Local: &CacheFromLocal{Src: "/foo/bar"}},
want: "type=local,src=/foo/bar",
},
{
name: "to-local",
given: CacheTo{Local: &CacheToLocal{Dest: "/foo/bar"}},
want: "type=local,dest=/foo/bar",
},
{
name: "inline",
given: CacheTo{Inline: &CacheToInline{}},
want: "type=inline",
},
{
name: "raw",
given: CacheTo{Raw: Raw("type=gha")},
want: "type=gha",
},
{
name: "compression",
given: CacheTo{Local: &CacheToLocal{
Dest: "/foo",
CacheWithCompression: CacheWithCompression{
Compression: &gzip,
CompressionLevel: 100,
ForceCompression: pulumi.BoolRef(true),
},
}},
want: "type=local,dest=/foo,compression=gzip,compression-level=22,force-compression=true",
},
{
name: "ignore-error",
given: CacheTo{
AZBlob: &CacheToAzureBlob{
CacheWithIgnoreError: CacheWithIgnoreError{pulumi.BoolRef(true)},
},
},
want: "type=azblob,ignore-error=true",
},
{
name: "oci",
given: CacheTo{
Registry: &CacheToRegistry{
CacheFromRegistry: CacheFromRegistry{Ref: "docker.io/foo/bar:baz"},
CacheWithOCI: CacheWithOCI{
OCI: pulumi.BoolRef(true),
ImageManifest: pulumi.BoolRef(true),
},
},
},
want: "type=registry,ref=docker.io/foo/bar:baz,oci-mediatypes=true,image-manifest=true",
},
{
name: "disabled-to",
given: CacheTo{
Raw: Raw("type=gha"),
Disabled: true,
},
want: "",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := tt.given.String()
assert.Equal(t, tt.want, actual)
if tt.want != "" {
// Our output should be parsable by Docker.
_, err := buildflags.ParseCacheEntry([]string{actual})
assert.NoError(t, err)
}
})
}
}

420
provider/internal/cli.go Normal file
View File

@@ -0,0 +1,420 @@
// 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.
//go:generate go run go.uber.org/mock/mockgen -typed -package internal -source cli.go -destination mockcli_test.go --self_package github.com/pulumi/pulumi-docker-build/provider/internal
package internal
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/docker/buildx/commands"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/credentials"
cfgtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/streams"
"github.com/moby/buildkit/client"
cp "github.com/otiai10/copy"
"github.com/regclient/regclient"
"github.com/regclient/regclient/config"
"github.com/sirupsen/logrus"
provider "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// cli wraps a DockerCLI instance with scoped auth credentials. It satisfies
// the Cli interface so it can be used with Docker's cobra.Commands directly.
//
// It buffers stdout/stderr, and layers temporary auth configs on top of the
// host's existing auth.
type cli struct {
command.Cli
auths map[string]cfgtypes.AuthConfig
host *host
in string // stdin
r, w *os.File // stdout
err bytes.Buffer // stderr
dumplogs bool // if true then tail() will re-log status messages
done chan struct{} // signaled when all logs have been forwarded to the engine.
}
// Cli wraps the Docker interface for mock generation.
type Cli interface {
command.Cli
}
// wrap creates a new cli client with auth configs layered on top of our host's
// auth. Repeated auth for the same host will take precedence over earlier
// credentials.
func wrap(host *host, registries ...Registry) (*cli, error) {
// We need to create a new DockerCLI instance because we don't want the
// auth changes we make to the ConfigFile to leak to the host.
docker, err := newDockerCLI(host.config)
if err != nil {
return nil, err
}
auths := map[string]cfgtypes.AuthConfig{}
for k, v := range host.auths {
if k != config.DockerRegistryAuth {
k = credentials.ConvertToHostname(k)
}
auths[k] = cfgtypes.AuthConfig{
ServerAddress: v.ServerAddress,
Username: v.Username,
Password: v.Password,
}
}
for _, r := range registries {
// HostNewName takes care of DockerHub's special-casing for us.
h := config.HostNewName(credentials.ConvertToHostname(r.Address))
key := h.CredHost
if key == "" {
key = h.Hostname
}
auths[key] = cfgtypes.AuthConfig{
ServerAddress: h.Hostname,
Username: r.Username,
Password: r.Password,
}
}
// Override our config's auth and disable any credential helpers. Auth
// lookups will now only return whatever we have in memory.
cfg := docker.ConfigFile()
cfg.AuthConfigs = auths
cfg.CredentialHelpers = nil
cfg.CredentialsStore = ""
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
wrapped := &cli{
Cli: docker,
host: host,
auths: auths,
r: r,
w: w,
}
return wrapped, nil
}
func (c *cli) In() *streams.In {
return streams.NewIn(io.NopCloser(strings.NewReader(c.in)))
}
func (c *cli) Out() *streams.Out {
return streams.NewOut(c.w)
}
func (c *cli) Err() io.Writer {
return &c.err
}
// rc returns a registry client with matching auth.
func (c *cli) rc() *regclient.RegClient {
hosts := []config.Host{}
for k, v := range c.auths {
h := config.HostNewName(k)
h.User = v.Username
h.Pass = v.Password
hosts = append(hosts, *h)
}
return regclient.New(
regclient.WithConfigHost(hosts...),
)
}
// tail is meant to be called as a goroutine and will pipe output from the CLI
// back to the Pulumi engine. Requires a corresponding call to close.
func (c *cli) tail(ctx provider.Context) {
c.done = make(chan struct{}, 1)
defer func() {
c.done <- struct{}{}
if err := recover(); err != nil {
fmt.Fprintf(os.Stderr, "recovered: %s\n", err)
}
}()
b := bytes.Buffer{}
s := bufio.NewScanner(c.r)
for s.Scan() {
text := s.Text()
ctx.LogStatus(diag.Info, text)
_, _ = b.WriteString(text + "\n")
}
ctx.LogStatus(diag.Info, "") // clear confusing "DONE" statements.
if c.dumplogs {
// Persist the full Docker output on error for easier debugging.
if b.Len() > 0 {
ctx.Log(diag.Info, b.String())
}
if c.err.Len() > 0 {
ctx.Log(diag.Error, c.err.String())
}
}
}
// close flushes any outstanding logs and cleans up resources.
func (c *cli) Close() error {
err := errors.Join(c.w.Close(), c.r.Close())
if c.done != nil {
<-c.done
}
return err
}
// execBuild performs a build by os.Exec'ing the docker-buildx binary.
// Credentials are communicated to docker-buildx via a temporary directory.
// Secrets are communicated via dynamic environment variables.
func (c *cli) execBuild(b Build) (*client.SolveResponse, error) {
// Setup a temporary directory for auth, and clean it up when we're done.
tmp, err := os.MkdirTemp("", "pulumi-docker-")
if err != nil {
return nil, err
}
defer contract.IgnoreError(os.RemoveAll(tmp))
opts := b.BuildOptions()
builder, err := c.host.builderFor(b)
if err != nil {
return nil, err
}
// Docker expects a "$DOCKER_CONFIG/contexts" directory in addition to
// "$DOCKER_CONFIG/config.json", so we attempt to copy this from the host
// to our temporary directory. This doesn't always exist, so we ignore errors.
hostConfigDir := filepath.Dir(c.ConfigFile().Filename)
_ = cp.Copy(
filepath.Join(hostConfigDir, "contexts"),
filepath.Join(tmp, "contexts"),
)
// Save our temporary credentials to $tmp/config.json.
tmpCfg := filepath.Join(tmp, filepath.Base(c.ConfigFile().Filename))
c.ConfigFile().Filename = tmpCfg
err = c.ConfigFile().Save()
if err != nil {
return nil, err
}
// We will spawn docker-buildx with DOCKER_CONFIG set to our temporary
// directory for auth, but BUILDX_CONFIG will point to the host. There's a
// bunch of builder state in there that we want to preserve.
env := []string{
"DOCKER_CONFIG=" + tmp,
"BUILDX_CONFIG=" + filepath.Join(hostConfigDir, "buildx"),
}
// We need to write to this file in order to recover information about the
// build, like the digest.
metadata := filepath.Clean(filepath.Join(tmp, "metadata.json"))
args := []string{
"buildx",
"build",
"--progress", "plain",
"--metadata-file", metadata,
"--builder", builder.name,
}
// TODO: --allow
// TODO: --annotation
// TODO: --attest
// TODO: --cgroup-parent
for k, v := range opts.BuildArgs {
args = append(args, "--build-arg", fmt.Sprintf("%s=%s", k, v))
}
if opts.Builder != "" {
args = append(args, "--builder", opts.Builder)
}
for _, c := range opts.CacheFrom {
args = append(args, "--cache-from", attrcsv(c.Type, c.Attrs))
}
for _, c := range opts.CacheTo {
args = append(args, "--cache-to", attrcsv(c.Type, c.Attrs))
}
if opts.ExportLoad {
args = append(args, "--load")
}
if opts.ExportPush {
args = append(args, "--push")
}
for _, e := range opts.Exports {
args = append(args, "--output", attrcsv(e.Type, e.Attrs))
}
for _, h := range opts.ExtraHosts {
args = append(args, "--add-host", h)
}
for k, v := range opts.NamedContexts {
args = append(args, "--build-context", fmt.Sprintf("%s=%s", k, v))
}
for k, v := range opts.Labels {
args = append(args, "--label", fmt.Sprintf("%s=%s", k, v))
}
if opts.NetworkMode != "" {
args = append(args, "--network", opts.NetworkMode)
}
if opts.NoCache {
args = append(args, "--no-cache")
}
for _, p := range opts.Platforms {
args = append(args, "--platform", p)
}
if opts.Pull {
args = append(args, "--pull")
}
for _, ssh := range opts.SSH {
s := ssh.ID
if len(ssh.Paths) > 0 {
s += "=" + strings.Join(ssh.Paths, ",")
}
args = append(args, "--ssh", s)
}
for _, t := range opts.Tags {
args = append(args, "--tag", t)
}
if opts.Target != "" {
args = append(args, "--target", opts.Target)
}
if opts.DockerfileName != "" {
args = append(args, "-f", opts.DockerfileName)
}
if in := b.Inline(); in != "" {
c.in = in
args = append(args, "-f", "-")
}
if opts.ContextPath != "" {
args = append(args, opts.ContextPath)
}
// We pass secrets by value via dynamic PULUMI_DOCKER_* environment
// variables.
for _, s := range opts.Secrets {
envvar, err := resource.NewUniqueHex("PULUMI_DOCKER_", 0, 0)
if err != nil {
return nil, err
}
// We abuse the pb.Secret proto by stuffing the secret's value in
// XXX_unrecognized. We never serialize this proto so this is tolerable.
env = append(env, fmt.Sprintf("%s=%s", envvar, s.XXX_unrecognized))
args = append(args, "--secret", fmt.Sprintf("id=%s,env=%s", s.ID, envvar))
}
// Invoke docker-buildx.
err = c.exec(args, env)
if err != nil {
return nil, err
}
// Read the metadata file and transform it back into the map[string]string
// structure originally returned by the exporter.
_, err = os.Stat(metadata)
if err != nil {
return nil, fmt.Errorf("missing metadata: %w", err)
}
out, err := os.ReadFile(metadata)
if err != nil {
return nil, err
}
var raw map[string]any
err = json.Unmarshal(out, &raw)
if err != nil {
return nil, err
}
resp := map[string]string{}
for k, v := range raw {
switch vv := v.(type) {
case string:
resp[k] = vv
default:
out, err := json.Marshal(v)
if err != nil {
continue
}
resp[k] = string(out)
}
}
return &client.SolveResponse{ExporterResponse: resp}, nil
}
// exec invokes a Docker plugin binary. The first argument should be the name
// of the plugin's subcommand, e.g. "buildx".
func (c *cli) exec(args, extraEnv []string) error {
if len(args) == 0 {
return errors.New("args must be non-empty")
}
name := args[0]
root := commands.NewRootCmd(name, false, c)
plug, err := manager.GetPlugin(name, c, root)
if err != nil {
return err
}
if plug.Err != nil {
return plug.Err
}
defer contract.IgnoreClose(c.w)
cmd, err := manager.PluginRunCommand(c, name, root)
if err != nil {
return err
}
cmd.Args = append([]string{cmd.Args[0]}, args...)
cmd.Stderr = c.Err()
cmd.Stdout = c.Out()
cmd.Stdin = c.In()
cmd.Env = append(cmd.Env, extraEnv...)
return cmd.Run()
}
// attrcsv transforms key/values into a CSV: key1=value1,key2=value2,...
func attrcsv(typ string, m map[string]string) string {
s := []string{"type=" + typ}
for k, v := range m {
s = append(s, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(s, ",")
}
func init() {
// Disable the CLI's tendency to log randomly to stdout.
logrus.SetOutput(io.Discard)
}

View File

@@ -0,0 +1,108 @@
// 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 (
"io"
"testing"
"github.com/docker/cli/cli/config/types"
"github.com/regclient/regclient/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExec(t *testing.T) {
t.Parallel()
h, err := newHost(nil)
require.NoError(t, err)
cli, err := wrap(h)
require.NoError(t, err)
err = cli.exec([]string{"buildx", "version"}, nil)
assert.NoError(t, err)
out, err := io.ReadAll(cli.r)
require.NoError(t, err)
assert.Contains(t, string(out), "github.com/docker/buildx")
}
func TestWrappedAuth(t *testing.T) {
t.Parallel()
ecr := "https://1234.dkr.ecr.us-west-2.amazonaws.com"
realhost, err := newHost(nil)
require.NoError(t, err)
h := &host{
auths: map[string]types.AuthConfig{
ecr: {
Username: "host-aws-user",
Password: "host-aws-password",
ServerAddress: ecr,
},
"https://misc": { // Legacy config includes http/https scheme.
Username: "host-misc-user",
Password: "host-misc-password",
ServerAddress: "misc",
},
},
}
registries := []Registry{
{
Address: "1234.dkr.ecr.us-west-2.amazonaws.com",
Username: "resource-aws-user",
Password: "resource-aws-password",
},
{
Address: "docker.io",
Username: "resource-dockerhub-user",
Password: "resource-dockerhub-password",
},
}
_, err = wrap(h, registries...)
require.NoError(t, err)
cli, err := wrap(h, registries...)
require.NoError(t, err)
expected := map[string]types.AuthConfig{
"1234.dkr.ecr.us-west-2.amazonaws.com": {
Username: "resource-aws-user",
Password: "resource-aws-password",
ServerAddress: "1234.dkr.ecr.us-west-2.amazonaws.com",
},
config.DockerRegistryAuth: {
Username: "resource-dockerhub-user",
Password: "resource-dockerhub-password",
ServerAddress: config.DockerRegistryDNS,
},
"misc": {
Username: "host-misc-user",
Password: "host-misc-password",
ServerAddress: "misc",
},
}
assert.Equal(t, expected, cli.auths)
assert.Len(t, h.auths, 2) // In-memory host auth is unchanged.
// Assert that our on-disk host's auth is untouched.
realhostRefreshed, err := newHost(nil)
require.NoError(t, err)
assert.Equal(t, realhost.auths, realhostRefreshed.auths)
}

367
provider/internal/client.go Normal file
View File

@@ -0,0 +1,367 @@
// 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.
//go:generate go run go.uber.org/mock/mockgen -typed -package internal -source client.go -destination mockclient_test.go --self_package github.com/pulumi/pulumi-docker-build/provider/internal
package internal
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/distribution/reference"
buildx "github.com/docker/buildx/build"
"github.com/docker/buildx/commands"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/util/dockerutil"
"github.com/docker/buildx/util/platformutil"
"github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/docker/docker/api/types/image"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/regclient/regclient/types/descriptor"
"github.com/regclient/regclient/types/errs"
"github.com/regclient/regclient/types/manifest"
"github.com/regclient/regclient/types/ref"
provider "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// Client handles all our Docker API calls.
type Client interface {
Build(ctx provider.Context, b Build) (*client.SolveResponse, error)
BuildKitEnabled() (bool, error)
Inspect(ctx context.Context, id string) ([]descriptor.Descriptor, error)
Delete(ctx context.Context, id string) error
ManifestCreate(ctx provider.Context, push bool, target string, refs ...string) error
ManifestInspect(ctx provider.Context, target string) (string, error)
ManifestDelete(ctx provider.Context, target string) error
}
// Build encapsulates all of the user-provider build parameters and options.
type Build interface {
BuildOptions() controllerapi.BuildOptions
Inline() string
ShouldExec() bool
Secrets() session.Attachable
}
var _ Client = (*cli)(nil)
func newDockerCLI(config *Config) (*command.DockerCli, error) {
cli, err := command.NewDockerCli(
command.WithDefaultContextStoreConfig(),
command.WithContentTrustFromEnv(),
)
if err != nil {
return nil, err
}
opts := flags.NewClientOptions()
if config != nil && config.Host != "" {
opts.Hosts = append(opts.Hosts, config.Host)
}
err = cli.Initialize(opts)
if err != nil {
return nil, err
}
// TODO: Log some version information for debugging.
return cli, nil
}
// Build performs a BuildKit build. Returns a map of target names (or one name,
// "default", if no targets were specified) to SolveResponses, which capture
// the build's digest and tags (if any).
func (c *cli) Build(
pctx provider.Context,
build Build,
) (*client.SolveResponse, error) {
ctx := context.Context(pctx)
opts := build.BuildOptions()
go c.tail(pctx)
defer contract.IgnoreClose(c)
if build.ShouldExec() {
return c.execBuild(build)
}
b, err := c.host.builderFor(build)
if err != nil {
return nil, err
}
printer, err := progress.NewPrinter(ctx, c.w,
progressui.PlainMode,
progress.WithDesc(
fmt.Sprintf("building with %q instance using %s driver", b.name, b.driver),
fmt.Sprintf("%s:%s", b.driver, b.name),
),
)
if err != nil {
return nil, fmt.Errorf("creating printer: %w", err)
}
defer func() {
// Log any warnings when we're done.
_ = printer.Wait()
for _, w := range printer.Warnings() {
b := &bytes.Buffer{}
fmt.Fprint(b, w.Short)
for _, d := range w.Detail {
fmt.Fprintf(b, "\n%s", d)
}
pctx.Log(diag.Warning, b.String())
}
}()
cacheFrom := []client.CacheOptionsEntry{}
for _, c := range opts.CacheFrom {
if c == nil {
continue
}
cacheFrom = append(cacheFrom, client.CacheOptionsEntry{
Type: c.Type,
Attrs: c.Attrs,
})
}
cacheTo := []client.CacheOptionsEntry{}
for _, c := range opts.CacheTo {
if c == nil {
continue
}
cacheTo = append(cacheTo, client.CacheOptionsEntry{
Type: c.Type,
Attrs: c.Attrs,
})
}
exports := []client.ExportEntry{}
for _, e := range opts.Exports {
if e == nil {
continue
}
exports = append(exports, client.ExportEntry{
Type: e.Type,
Attrs: e.Attrs,
OutputDir: e.Destination,
})
}
platforms, _ := platformutil.Parse(opts.Platforms)
platforms = platformutil.Dedupe(platforms)
namedContexts := map[string]buildx.NamedContext{}
for k, v := range opts.NamedContexts {
ref, err := reference.ParseNormalizedNamed(k)
if err != nil {
return nil, err
}
name := strings.TrimSuffix(reference.FamiliarString(ref), ":latest")
namedContexts[name] = buildx.NamedContext{Path: v}
}
ssh, err := controllerapi.CreateSSH(opts.SSH)
if err != nil {
return nil, err
}
target := opts.Target
if target == "" {
target = "default"
}
payload := map[string]buildx.Options{
target: {
Inputs: buildx.Inputs{
ContextPath: opts.ContextPath,
DockerfilePath: opts.DockerfileName,
DockerfileInline: build.Inline(),
NamedContexts: namedContexts,
InStream: strings.NewReader(""),
},
// Disable default provenance for now. Docker's `manifest create`
// doesn't handle manifests with provenance included; more reason
// to use imagetools instead.
Attests: map[string]*string{"provenance": nil},
BuildArgs: opts.BuildArgs,
CacheFrom: cacheFrom,
CacheTo: cacheTo,
Exports: exports,
ExtraHosts: opts.ExtraHosts,
NetworkMode: opts.NetworkMode,
NoCache: opts.NoCache,
Labels: opts.Labels,
Platforms: platforms,
Pull: opts.Pull,
Tags: opts.Tags,
Target: opts.Target,
Session: []session.Attachable{
ssh,
authprovider.NewDockerAuthProvider(c.ConfigFile(), nil),
build.Secrets(),
},
},
}
// Perform the build.
results, err := buildx.Build(
ctx,
b.nodes,
payload,
dockerutil.NewClient(c),
filepath.Dir(c.ConfigFile().Filename),
printer,
)
if err != nil {
c.dumplogs = true
return nil, err
}
return results[target], err
}
// BuildKitEnabled returns true if the client supports buildkit.
func (c *cli) BuildKitEnabled() (bool, error) {
return c.Cli.BuildKitEnabled()
}
func (c *cli) ManifestCreate(ctx provider.Context, push bool, target string, refs ...string) error {
go c.tail(ctx)
defer contract.IgnoreClose(c)
args := []string{
// "buildx",
"imagetools",
"create",
"--progress=plain",
"--tag", target,
}
if !push {
args = append(args, "--dry-run")
}
args = append(args, refs...)
cmd := commands.NewRootCmd(os.Args[0], false, c)
cmd.SetArgs(args)
cmd.SetErr(c.Err())
cmd.SetOut(c.Out())
ctx.Log(diag.Debug, fmt.Sprint("creating manifest with args", args))
return cmd.ExecuteContext(ctx)
}
func (c *cli) ManifestInspect(ctx provider.Context, target string) (string, error) {
rc := c.rc()
ref, err := ref.New(target)
if err != nil {
return "", err
}
m, err := rc.ManifestHead(ctx, ref)
if err != nil {
return "", fmt.Errorf("fetching %q: %w", ref, err)
}
return string(m.GetDescriptor().Digest), nil
}
func (c *cli) ManifestDelete(ctx provider.Context, target string) error {
rc := c.rc()
ref, err := ref.New(target)
if err != nil {
return err
}
err = rc.ManifestDelete(context.Context(ctx), ref)
if errors.Is(err, errs.ErrHTTPStatus) {
ctx.Log(diag.Warning, "this registry does not support deletions")
return nil
}
if err != nil {
return err
}
return nil
}
// Inspect inspects an image.
func (c *cli) Inspect(ctx context.Context, r string) ([]descriptor.Descriptor, error) {
ref, err := ref.New(r)
if err != nil {
return nil, err
}
rc := c.rc()
m, err := rc.ManifestGet(ctx, ref)
if err != nil {
return nil, err
}
if mi, ok := m.(manifest.Indexer); ok {
return mi.GetManifestList()
}
return []descriptor.Descriptor{m.GetDescriptor()}, nil
}
// Delete attempts to delete an image with the given ref. Many registries don't
// support the DELETE API yet, so this operation is not guaranteed to work.
func (c *cli) Delete(ctx context.Context, r string) error {
// Attempt to delete the ref locally if it exists.
_, _ = c.Client().ImageRemove(ctx, r, image.RemoveOptions{
Force: true, // Needed in case the image has multiple tags.
})
// Attempt to delete the ref remotely if it was pushed -- requires a
// digest.
ref, err := ref.New(r)
if err != nil || ref.Digest == "" {
return nil
}
rc := c.rc()
// TODO: Multi-platform manifests are left dangling on ECR.
_ = rc.ManifestDelete(ctx, ref)
return nil
}
func normalizeReference(ref string) (reference.Named, error) {
namedRef, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return nil, err
}
if _, isDigested := namedRef.(reference.Canonical); !isDigested {
return reference.TagNameOnly(namedRef), nil
}
return namedRef, nil
}

View File

@@ -0,0 +1,481 @@
// 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 (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"github.com/docker/docker/api/types/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
)
func TestAuth(t *testing.T) {
t.Parallel()
user := "pulumibot"
if u := os.Getenv("DOCKER_HUB_USER"); u != "" {
user = u
}
password := os.Getenv("DOCKER_HUB_PASSWORD")
address := "docker.io"
cli := testcli(t, true, Registry{
Address: address,
Username: user,
Password: password,
})
_, err := cli.Client().
RegistryLogin(context.Background(), registry.AuthConfig{ServerAddress: address})
assert.NoError(t, err)
}
func TestCustomHost(t *testing.T) {
socket := "unix:///foo/bar.sock"
//nolint:paralleltest // not compatible with Setenv
t.Run("env", func(t *testing.T) {
t.Setenv("DOCKER_HOST", socket)
h, err := newHost(nil)
require.NoError(t, err)
cli, err := wrap(h)
require.NoError(t, err)
assert.Equal(t, socket, cli.Client().DaemonHost())
assert.Equal(t, socket, cli.DockerEndpoint().Host)
})
t.Run("config", func(t *testing.T) {
t.Parallel()
h, err := newHost(&Config{Host: socket})
require.NoError(t, err)
cli, err := wrap(h)
require.NoError(t, err)
assert.Equal(t, socket, cli.Client().DaemonHost())
assert.Equal(t, socket, cli.DockerEndpoint().Host)
})
}
func TestBuild(t *testing.T) {
t.Parallel()
// Workaround for https://github.com/pulumi/pulumi-go-provider/issues/159
ctrl, ctx := gomock.WithContext(context.Background(), t)
pctx := NewMockProviderContext(ctrl)
pctx.EXPECT().Log(gomock.Any(), gomock.Any()).AnyTimes()
pctx.EXPECT().LogStatus(gomock.Any(), gomock.Any()).AnyTimes()
pctx.EXPECT().Done().Return(ctx.Done()).AnyTimes()
pctx.EXPECT().
Value(gomock.Any()).
DoAndReturn(func(key any) any { return ctx.Value(key) }).
AnyTimes()
pctx.EXPECT().Err().Return(ctx.Err()).AnyTimes()
pctx.EXPECT().Deadline().Return(ctx.Deadline()).AnyTimes()
tmpdir := t.TempDir()
max := Max
exampleContext := &BuildContext{Context: Context{Location: "../../examples/app"}}
tests := []struct {
name string
skip bool
args ImageArgs
auths []Registry
}{
{
name: "multiPlatform",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.multiPlatform",
},
Platforms: []Platform{"plan9/amd64", "plan9/arm64"},
},
},
{
name: "registryPush",
skip: os.Getenv("DOCKER_HUB_PASSWORD") == "",
args: ImageArgs{
Context: exampleContext,
Tags: []string{"docker.io/pulumibot/buildkit-e2e:unit"},
Push: true,
},
auths: []Registry{{
Address: "docker.io",
Username: "pulumibot",
Password: os.Getenv("DOCKER_HUB_PASSWORD"),
}},
},
{
name: "cached",
args: ImageArgs{
Context: exampleContext,
Tags: []string{"cached"},
CacheTo: []CacheTo{{Local: &CacheToLocal{
Dest: filepath.Join(tmpdir, "cache"),
CacheWithMode: CacheWithMode{Mode: &max},
}}},
CacheFrom: []CacheFrom{{Local: &CacheFromLocal{
Src: filepath.Join(tmpdir, "cache"),
}}},
},
},
{
name: "buildArgs",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.buildArgs",
},
BuildArgs: map[string]string{
"SET_ME_TO_TRUE": "true",
},
},
},
{
name: "extraHosts",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.extraHosts",
},
AddHosts: []string{
"metadata.google.internal:169.254.169.254",
},
},
},
{
name: "sshMount",
skip: os.Getenv("SSH_AUTH_SOCK") == "",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.sshMount",
},
SSH: []SSH{{ID: "default"}},
},
},
{
name: "secrets",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.secrets",
},
Secrets: map[string]string{
"password": "hunter2",
},
NoCache: true,
},
},
{
name: "labels",
args: ImageArgs{
Context: exampleContext,
Labels: map[string]string{
"description": "foo",
},
},
},
{
name: "target",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.target",
},
Target: "build-me",
},
},
{
name: "namedContext",
args: ImageArgs{
Context: &BuildContext{
Context: Context{
Location: "../../examples/app",
},
Named: NamedContexts{
"golang:latest": Context{
Location: "docker-image://golang@sha256:b8e62cf593cdaff36efd90aa3a37de268e6781a2e68c6610940c48f7cdf36984",
},
},
},
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.namedContexts",
},
},
},
{
name: "remoteContext",
args: ImageArgs{
Context: &BuildContext{
Context: Context{
Location: "https://raw.githubusercontent.com/pulumi/pulumi-docker/api-types/provider/testdata/Dockerfile",
},
},
},
},
{
name: "remoteContextWithInline",
args: ImageArgs{
Context: &BuildContext{
Context: Context{
Location: "https://github.com/docker-library/hello-world.git",
},
},
Dockerfile: &Dockerfile{
Inline: dedent(`
FROM busybox
COPY hello.c ./
`),
},
},
},
{
name: "inline",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Inline: dedent(`
FROM alpine
RUN echo 👍
`),
},
},
},
{
name: "dockerLoad",
args: ImageArgs{
Context: exampleContext,
Load: true,
},
},
}
// Add an exec: true version for all of our test cases.
for _, tt := range tests {
tt := tt
tt.name = "exec-" + tt.name
tt.args.Exec = true
tmpdir := filepath.Join(t.TempDir(), "exec")
for _, c := range tt.args.CacheTo {
if c.Local != nil {
c.Local.Dest = tmpdir
}
}
for _, c := range tt.args.CacheFrom {
if c.Local != nil {
c.Local.Src = tmpdir
}
}
tests = append(tests, tt)
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.skip {
t.Skip()
}
cli := testcli(t, true, tt.auths...)
build, err := tt.args.toBuild(pctx, false)
require.NoError(t, err)
_, err = cli.Build(pctx, build)
assert.NoError(t, err, cli.err.String())
})
}
}
func TestBuildkitEnabled(t *testing.T) {
t.Parallel()
cli := testcli(t, false)
ok, err := cli.BuildKitEnabled()
assert.NoError(t, err)
assert.True(t, ok)
}
func TestInspect(t *testing.T) {
t.Parallel()
cli := testcli(t, false)
descriptors, err := cli.Inspect(context.Background(), "pulumibot/myapp:buildx")
require.NoError(t, err)
assert.Equal(
t,
"application/vnd.docker.distribution.manifest.v2+json",
descriptors[0].MediaType,
)
}
func TestNormalizeReference(t *testing.T) {
t.Parallel()
tests := []struct {
ref string
want string
wantErr string
}{
{
ref: "foo",
want: "docker.io/library/foo:latest",
},
{
ref: "pulumi/pulumi:v3.100.0",
want: "docker.io/pulumi/pulumi:v3.100.0",
},
{
ref: "invalid:ref:format",
wantErr: "invalid reference format",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.ref, func(t *testing.T) {
t.Parallel()
ref, err := normalizeReference(tt.ref)
if err != nil {
assert.ErrorContains(t, err, tt.wantErr)
} else {
assert.Equal(t, ref.String(), tt.want)
}
})
}
}
func TestBuildError(t *testing.T) {
t.Parallel()
if os.Getenv("CI") != "" {
t.Skip("flaky on CI for some reason")
}
ctrl, ctx := gomock.WithContext(context.Background(), t)
exampleContext := &BuildContext{Context: Context{Location: "../../examples/app"}}
args := ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Inline: "FROM alpine\nRUN echo hello\nRUN badcmd",
},
}
logged := bytes.Buffer{}
pctx := NewMockProviderContext(ctrl)
pctx.EXPECT().Done().Return(ctx.Done()).AnyTimes()
pctx.EXPECT().
Value(gomock.Any()).
DoAndReturn(func(key any) any { return ctx.Value(key) }).
AnyTimes()
pctx.EXPECT().Err().Return(ctx.Err()).AnyTimes()
pctx.EXPECT().Deadline().Return(ctx.Deadline()).AnyTimes()
pctx.EXPECT().LogStatus(gomock.Any(), gomock.Any()).AnyTimes()
pctx.EXPECT().Log(gomock.Any(), gomock.Any()).DoAndReturn(func(_ diag.Severity, msg string) {
logged.WriteString(msg)
}).AnyTimes()
cli := testcli(t, true)
build, err := args.toBuild(pctx, false)
require.NoError(t, err)
_, err = cli.Build(pctx, build)
assert.Error(t, err)
want := []string{
`RUN echo hello`,
`/bin/sh: badcmd: not found`,
}
for _, want := range want {
assert.Contains(t, logged.String(), want)
}
assert.ErrorContains(t, err,
`process "/bin/sh -c badcmd" did not complete successfully: exit code: 127`,
)
}
func TestBuildExecError(t *testing.T) {
t.Parallel()
ctrl, _ := gomock.WithContext(context.Background(), t)
exampleContext := &BuildContext{Context: Context{Location: "../../examples/app"}}
args := ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Inline: "FROM alpine\nRUN echo hello\nRUN badcmd",
},
Exec: true,
}
pctx := NewMockProviderContext(ctrl)
pctx.EXPECT().Log(
diag.Warning,
"No exports were specified so the build will only remain in the local build cache. "+
"Use `push` to upload the image to a registry, or silence this warning with a `cacheonly` export.",
)
pctx.EXPECT().LogStatus(gomock.Any(), gomock.Any()).AnyTimes()
cli := testcli(t, true)
build, err := args.toBuild(pctx, false)
require.NoError(t, err)
_, err = cli.Build(pctx, build)
assert.Error(t, err)
want := []string{
`RUN echo hello`,
`/bin/sh: badcmd: not found`,
`process "/bin/sh -c badcmd" did not complete successfully: exit code: 127`,
}
for _, want := range want {
assert.Contains(t, cli.err.String(), want)
}
}
// testcli returns a new standalone CLI instance. Set ping to true if a live
// daemon is required -- the test will be skipped if the daemon is not available.
func testcli(t *testing.T, ping bool, auths ...Registry) *cli {
h, err := newHost(nil)
require.NoError(t, err)
cli, err := wrap(h, auths...)
require.NoError(t, err)
if ping {
_, err := cli.Client().Ping(context.Background())
if err != nil {
t.Skip(err)
}
}
return cli
}

View File

@@ -0,0 +1,345 @@
// 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 (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
gofs "io/fs"
"os"
"path"
"path/filepath"
"syscall"
buildx "github.com/docker/buildx/build"
"github.com/moby/patternmatcher/ignorefile"
"github.com/spf13/afero"
"github.com/tonistiigi/fsutil"
"github.com/pulumi/pulumi-go-provider/infer"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
var (
_ infer.Annotated = (*Context)(nil)
_ infer.Annotated = (*BuildContext)(nil)
)
// Context represents Docker's `PATH | URL | -` context argument. Inline
// context isn't supported yet.
type Context struct {
Location string `pulumi:"location"` // Location is a local directory or URL.
}
// BuildContext represents Docker's named and unamed contexts.
type BuildContext struct {
Context
Named NamedContexts `pulumi:"named,optional"`
}
func (bc *BuildContext) namedMap() map[string]string {
if bc == nil {
return nil
}
return bc.Named.Map()
}
// NamedContexts correspond to Docker's `--build-context name=path` options.
// The path can be local or a remote URL.
type NamedContexts map[string]Context
// Map returns NamedContexts as a simple map.
func (nc NamedContexts) Map() map[string]string {
m := map[string]string{}
for k, v := range nc {
m[k] = v.Location
}
return m
}
// Annotate sets docstrings on Context.
func (c *Context) Annotate(a infer.Annotator) {
a.Describe(&c.Location, dedent(`
Resources to use for build context.
The location can be:
* A relative or absolute path to a local directory (".", "./app",
"/app", etc.).
* A remote URL of a Git repository, tarball, or plain text file
("https://github.com/user/myrepo.git", "http://server/context.tar.gz",
etc.).
`))
}
// validate returns a non-nil CheckError if the Context is invalid. The
// returned Dockerfile may have defaults set to match Docker's default
// handling. The returned Dockerfile should be validated separately.
func (bc *BuildContext) validate(preview bool, d *Dockerfile) (*Dockerfile, *Context, error) {
if d == nil {
d = &Dockerfile{}
}
c := &Context{}
if bc != nil {
c = &bc.Context
}
if c.Location == "" && preview {
// The field is required so we normally wouldn't need to check if it
// exists, but during previews it can still be empty if the value is
// unknown. This isn't an error, but it does prevent us from performing
// a build later.
return d, c, nil
}
if buildx.IsRemoteURL(c.Location) {
// We assume remote URLs are always valid.
return d, c, nil
}
abs, err := filepath.Abs(c.Location)
if err != nil {
return d, c, newCheckFailure(err, "context.location")
}
if d.Location == "" && d.Inline == "" {
// If a Dockerfile wasn't provided and our context is on-disk, then
// set our Dockerfile to a default of <PATH>/Dockerfile.
d.Location = filepath.Join(c.Location, "Dockerfile")
}
if isLocalDir(afero.NewOsFs(), abs) {
// Our context exists -- nothing else to check.
return d, c, nil
}
if c.Location != "-" {
return d, c, newCheckFailure(
fmt.Errorf("%q: not a valid directory or URL", c.Location),
"context.location",
)
}
return d, c, nil
}
// Annotate sets docstrings on BuildContext.
func (bc *BuildContext) Annotate(a infer.Annotator) {
a.Describe(&bc.Named, dedent(`
Additional build contexts to use.
These contexts are accessed with "FROM name" or "--from=name"
statements when using Dockerfile 1.4+ syntax.
Values can be local paths, HTTP URLs, or "docker-image://" images.
`))
}
// hashFile hashes a file's contents and accumulates it into the provider Hash.
func hashFile(
h hash.Hash,
fs fsutil.FS,
relativePath string,
fileMode gofs.FileMode,
) error {
if fileMode.IsDir() {
return nil
}
if !(fileMode.IsRegular() || fileMode.Type() == os.ModeSymlink) {
return nil
}
f, err := fs.Open(relativePath)
if err != nil {
return fmt.Errorf("could not open %q: %w", relativePath, err)
}
defer contract.IgnoreClose(f)
_, err = io.Copy(h, f)
if errors.Is(err, syscall.EISDIR) {
// Ignore symlinks to directories.
return nil
}
if err != nil {
return fmt.Errorf("could not copy %q to hash: %w", relativePath, err)
}
h.Write([]byte(filepath.ToSlash(path.Clean(relativePath))))
h.Write([]byte(fileMode.String()))
return nil
}
// hashBuildContext accumulates hashes for files in a directory. If the file is
// a symlink, the location it points to is hashed. If it is a regular file, we
// hash the contents of the file. In order to detect file renames and mode
// changes, we also write to the accumulator a relative name and file mode.
func hashBuildContext(
contextPath, dockerfilePath string,
namedContexts map[string]string,
) (string, error) {
h := sha256.New()
fs := afero.NewOsFs()
// Grab .dockerignore if our context and/or Dockerfile is on-disk.
excludes := []string{}
if isLocalDir(fs, contextPath) || isLocalFile(fs, dockerfilePath) {
e, err := getIgnorePatterns(fs, dockerfilePath, contextPath)
if err != nil {
return "", err
}
excludes = e
}
if isLocalFile(fs, dockerfilePath) {
err := hashDockerfile(h, dockerfilePath)
if err != nil {
return "", nil
}
}
if isLocalDir(fs, contextPath) {
// Hash our context if it's on-disk.
fs, err := rootFS(contextPath, excludes)
if err != nil {
return "", err
}
if _, err := hashPath(h, fs); err != nil {
return "", err
}
}
// Hash any local named contexts.
for _, namedContext := range namedContexts {
if isLocalDir(fs, namedContext) {
fs, err := rootFS(namedContext, excludes)
if err != nil {
return "", err
}
if _, err := hashPath(h, fs); err != nil {
return "", err
}
}
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// hashPath hashes all paths within the provided FS.
func hashPath(h hash.Hash, fs fsutil.FS) (string, error) {
err := fs.Walk(
context.Background(),
"/",
func(filePath string, dir gofs.DirEntry, err error) error {
if err != nil {
return err
}
if dir.IsDir() {
return nil
}
// fsutil.Walk makes filePath relative to the root, we join it back to get an absolute path to
// the file to hash.
fi, err := dir.Info()
if err != nil {
return err
}
return hashFile(h, fs, filePath, fi.Mode())
},
)
if err != nil {
return "", fmt.Errorf("unable to hash build context: %w", err)
}
// create a hash of the entire input of the hash accumulator
return hex.EncodeToString(h.Sum(nil)), nil
}
// hashDockerfile hashes the contents of a Dockerfile.
func hashDockerfile(h hash.Hash, path string) error {
// The Dockerfile might be capture by .dockerignore, so we explicitly hash
// its content (but not filename -- to match Docker) in order to detect
// changes in it.
df, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return fmt.Errorf("error reading dockerfile %q: %w", path, err)
}
_, err = h.Write(df)
if err != nil {
return fmt.Errorf("error hashing dockerfile %q: %w", path, err)
}
return nil
}
// getIgnorePatterns returns all patterns to ignore when constructing a build
// context for the given Dockerfile, if any such patterns exist.
//
// Precedence is given to Dockerfile-specific ignore-files as per
// https://docs.docker.com/build/building/context/#filename-and-location.
func getIgnorePatterns(fs afero.Fs, dockerfilePath, contextRoot string) ([]string, error) {
paths := []string{
// Prefer <Dockerfile>.dockerignore if it's present.
dockerfilePath + ".dockerignore",
}
if isLocalDir(fs, contextRoot) {
// Otherwise fall back to the ignore-file at the root of our build context.
paths = append(paths, filepath.Join(contextRoot, ".dockerignore"))
}
// Attempt to parse our candidate ignore-files, skipping any that don't
// exist.
for _, p := range paths {
f, err := fs.Open(p)
if errors.Is(err, afero.ErrFileNotFound) {
continue
}
if err != nil {
return nil, fmt.Errorf("reading %q: %w", p, err)
}
ignorePatterns, err := ignorefile.ReadAll(f)
if err != nil {
contract.IgnoreClose(f)
return nil, fmt.Errorf("unable to parse %q: %w", p, err)
}
contract.IgnoreClose(f)
return ignorePatterns, nil
}
return nil, nil
}
func isLocalDir(fs afero.Fs, path string) bool {
stat, err := fs.Stat(path)
return err == nil && stat.IsDir()
}
func isLocalFile(fs afero.Fs, path string) bool {
stat, err := fs.Stat(path)
return err == nil && !stat.IsDir()
}
// rootFS returns a new fsutil.FS scoped to the given root and with the given
// exclusions.
func rootFS(root string, excludes []string) (fsutil.FS, error) {
fs, err := fsutil.NewFS(root)
if err != nil {
return nil, err
}
return fsutil.NewFilterFS(fs, &fsutil.FilterOpt{ExcludePatterns: excludes})
}

View File

@@ -0,0 +1,417 @@
// 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 (
"bufio"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var _dockerfile = "Dockerfile"
func TestValidateContext(t *testing.T) {
t.Parallel()
tests := []struct {
name string
c Context
givenD Dockerfile
preview bool
wantD *Dockerfile
wantErr string
}{
{
name: "relative",
c: Context{
Location: "../internal/../internal/testdata/noop",
},
wantD: &Dockerfile{
Location: "../internal/testdata/noop/Dockerfile",
},
},
{
name: "missing directory",
c: Context{
Location: "/does/not/exist/",
},
wantErr: "not a valid directory",
},
{
name: "missing default Dockerfile",
c: Context{
Location: "testdata",
},
wantD: &Dockerfile{Location: "testdata/Dockerfile"},
},
{
name: "with explicit Dockerfile",
c: Context{
Location: "testdata",
},
givenD: Dockerfile{
Location: "testdata/Dockerfile.invalid",
},
},
{
name: "default location",
c: Context{},
wantD: &Dockerfile{Location: "Dockerfile"},
},
{
name: "remote context doesn't default to local Dockerfile",
c: Context{
Location: "https://raw.githubusercontent.com/pulumi/pulumi-docker/api-types/provider/testdata/Dockerfile",
},
wantD: &Dockerfile{},
},
{
name: "preview",
c: Context{},
preview: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
bc := &BuildContext{Context: tt.c}
d, _, err := bc.validate(tt.preview, &tt.givenD)
if tt.wantErr == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.wantErr)
}
if tt.wantD != nil {
assert.Equal(t, tt.wantD.Location, d.Location)
assert.Equal(t, tt.wantD.Inline, d.Inline)
}
})
}
}
func TestHashIgnoresFile(t *testing.T) {
t.Parallel()
step1Dir := "./testdata/ignores/basedir"
baseResult, err := hashBuildContext(step1Dir, filepath.Join(step1Dir, _dockerfile), nil)
require.NoError(t, err)
step2Dir := "./testdata/ignores/basedir-with-ignored-files"
result, err := hashBuildContext(step2Dir, filepath.Join(step2Dir, _dockerfile), nil)
require.NoError(t, err)
assert.Equal(t, result, baseResult)
}
// Tests that we handle .dockerignore exclusions such as "!foo/*/bar".
//
// See:
// - https://github.com/moby/moby/issues/30018
// - https://github.com/moby/moby/issues/45608
//
// Buildkit handles these correctly (according to spec), Docker's classic builder does not.
func TestHashIgnoresWildcards(t *testing.T) {
t.Parallel()
baselineDir := "testdata/ignores-wildcard/basedir"
baselineResult, err := hashBuildContext(
baselineDir,
filepath.Join(baselineDir, _dockerfile),
nil,
)
require.NoError(t, err)
modIgnoredDir := "testdata/ignores-wildcard/basedir-modified-ignored-file"
modIgnoredResult, err := hashBuildContext(
modIgnoredDir,
filepath.Join(modIgnoredDir, _dockerfile),
nil,
)
require.NoError(t, err)
modIncludedDir := "testdata/ignores-wildcard/basedir-modified-included-file"
modIncludedResult, err := hashBuildContext(
modIncludedDir,
filepath.Join(modIncludedDir, _dockerfile),
nil,
)
require.NoError(t, err)
assert.Equal(
t,
baselineResult,
modIgnoredResult,
"hash should not change when modifying ignored files",
)
assert.NotEqual(t, baselineResult, modIncludedResult,
"hash should change when modifying included (via wildcard ignore exclusion) files")
}
func BenchmarkHashBuildContext(b *testing.B) {
dir := "testdata/ignores-wildcard/basedir-modified-ignored-file"
for n := 0; n < b.N; n++ {
_, err := hashBuildContext(dir, filepath.Join(dir, _dockerfile), nil)
require.NoError(b, err)
}
}
// Tests that we handle .dockerignore exclusions such as "!foo/*/bar", as above, when using a
// relative context path.
//
//nolint:paralleltest // Incompatible with os.Chdir.
func TestHashIgnoresWildcardsRelative(t *testing.T) {
err := os.Chdir("testdata")
require.NoError(t, err)
defer func() {
err = os.Chdir("..")
require.NoError(t, err)
}()
baselineDir := "../testdata/ignores-wildcard/basedir"
baselineResult, err := hashBuildContext(
baselineDir,
filepath.Join(baselineDir, _dockerfile),
nil,
)
require.NoError(t, err)
modIgnoredDir := "../testdata/ignores-wildcard/basedir-modified-ignored-file"
modIgnoredResult, err := hashBuildContext(
modIgnoredDir,
filepath.Join(modIgnoredDir, _dockerfile),
nil,
)
require.NoError(t, err)
modIncludedDir := "../testdata/ignores-wildcard/basedir-modified-included-file"
modIncludedResult, err := hashBuildContext(
modIncludedDir,
filepath.Join(modIncludedDir, _dockerfile),
nil,
)
require.NoError(t, err)
assert.Equal(
t,
baselineResult,
modIgnoredResult,
"hash should not change when modifying ignored files",
)
assert.NotEqual(t, baselineResult, modIncludedResult,
"hash should change when modifying included (via wildcard ignore exclusion) files")
}
func TestHashIgnoresDockerfileOutsideDirMove(t *testing.T) {
t.Parallel()
appDir := "./testdata/dockerfile-location-irrelevant/app"
baseResult, err := hashBuildContext(
appDir,
"./testdata/dockerfile-location-irrelevant/step1.Dockerfile",
nil,
)
require.NoError(t, err)
result, err := hashBuildContext(
appDir,
"./testdata/dockerfile-location-irrelevant/step2.Dockerfile",
nil,
)
require.NoError(t, err)
assert.Equal(t, result, baseResult)
}
func TestHashRenamingMatters(t *testing.T) {
t.Parallel()
step1Dir := "./testdata/filemode-matters/step1"
baseResult, err := hashBuildContext(step1Dir, filepath.Join(step1Dir, _dockerfile), nil)
require.NoError(t, err)
step2Dir := "./testdata/renaming-matters/step2"
result, err := hashBuildContext(step2Dir, filepath.Join(step2Dir, _dockerfile), nil)
require.NoError(t, err)
assert.NotEqual(t, result, baseResult)
}
func TestHashFilemodeMatters(t *testing.T) {
t.Parallel()
step1Dir := "./testdata/filemode-matters/step1"
baseResult, err := hashBuildContext(step1Dir, filepath.Join(step1Dir, _dockerfile), nil)
require.NoError(t, err)
step2Dir := "./testdata/filemode-matters/step2-chmod-x"
result, err := hashBuildContext(step2Dir, filepath.Join(step2Dir, _dockerfile), nil)
require.NoError(t, err)
assert.NotEqual(t, result, baseResult)
}
func TestHashDeepSymlinks(t *testing.T) {
t.Parallel()
dir := "./testdata/symlinks"
_, err := hashBuildContext(dir, filepath.Join(dir, "Dockerfile"), nil)
assert.NoError(t, err)
}
func TestIgnoreIrregularFiles(t *testing.T) {
t.Parallel()
dir := t.TempDir()
// Create a Dockerfile
dockerfile := filepath.Join(dir, "Dockerfile")
err := os.WriteFile(dockerfile, []byte{}, 0o600)
require.NoError(t, err)
// Create a pipe which should be ignored. (We will time out trying to read
// it if it's not.)
pipe := filepath.Join(dir, "pipe")
err = syscall.Mkfifo(pipe, 0o666)
require.NoError(t, err)
// Confirm it's irregular.
fi, err := os.Stat(pipe)
require.NoError(t, err)
assert.False(t, fi.Mode().IsRegular())
_, err = hashBuildContext(dir, dockerfile, nil)
assert.NoError(t, err)
}
func TestHashUnignoredDirs(t *testing.T) {
t.Parallel()
step1Dir := "./testdata/unignores/basedir"
baseResult, err := hashBuildContext(step1Dir, filepath.Join(step1Dir, _dockerfile), nil)
require.NoError(t, err)
step2Dir := "./testdata/unignores/basedir-with-unignored-files"
unignoreResult, err := hashBuildContext(step2Dir, filepath.Join(step2Dir, _dockerfile), nil)
require.NoError(t, err)
assert.Equal(t, baseResult, unignoreResult)
}
func TestDockerIgnore(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dockerfile string
context string
fs map[string]string
want []string
wantErr error
}{
{
name: "Dockerfile with root dockerignore",
dockerfile: "./foo/Dockerfile",
fs: map[string]string{
".dockerignore": "rootignore",
},
want: []string{"rootignore"},
},
{
name: "Dockerfile with root dockerignore and custom dockerignore",
dockerfile: "./foo/Dockerfile",
fs: map[string]string{
"foo/Dockerfile.dockerignore": "customignore",
".dockerignore": "rootignore",
},
want: []string{"customignore"},
},
{
name: "Dockerfile with root dockerignore and relative context",
dockerfile: "./foo/Dockerfile",
context: "../",
fs: map[string]string{
"../.dockerignore": "rootignore",
},
want: []string{"rootignore"},
},
{
name: "Dockerfile without root dockerignore",
dockerfile: "./foo/Dockerfile",
want: nil,
},
{
name: "Dockerfile with invalid root dockerignore",
dockerfile: "./foo/Dockerfile",
fs: map[string]string{
".dockerignore": strings.Repeat("*", bufio.MaxScanTokenSize),
},
wantErr: bufio.ErrTooLong,
},
{
name: "custom.Dockerfile without custom dockerignore and without root dockerignore",
dockerfile: "./foo/custom.Dockerfile",
want: nil,
},
{
name: "custom.Dockerfile with custom dockerignore and without root dockerignore",
dockerfile: "./foo/custom.Dockerfile",
fs: map[string]string{
"foo/custom.Dockerfile.dockerignore": "customignore",
},
want: []string{"customignore"},
},
{
name: "custom.Dockerfile with custom dockerignore and with root dockerignore",
dockerfile: "foo/custom.Dockerfile",
fs: map[string]string{
"foo/custom.Dockerfile.dockerignore": "customignore",
".dockerignore": "rootignore",
},
want: []string{"customignore"},
},
{
name: "custom.Dockerfile without custom dockerignore and with root dockerignore",
dockerfile: "foo/custom.Dockerfile",
fs: map[string]string{
".dockerignore": "rootignore",
},
want: []string{"rootignore"},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
for fname, fdata := range tt.fs {
f, err := fs.Create(fname)
require.NoError(t, err)
_, err = f.WriteString(fdata)
require.NoError(t, err)
}
actual, err := getIgnorePatterns(fs, tt.dockerfile, tt.context)
assert.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, actual)
})
}
}

View File

@@ -0,0 +1,870 @@
mode: set
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:34.47,36.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:38.65,40.9 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:40.9,42.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:43.2,44.9 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:44.9,46.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:47.2,48.9 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:48.9,50.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:51.2,51.46 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:51.46,53.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:54.2,55.18 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:60.3,62.27 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:62.27,64.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:68.2,68.13 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:68.13,70.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:72.2,73.62 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:73.62,75.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:77.2,81.59 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:81.59,83.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:83.16,85.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:87.3,88.23 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:88.23,90.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:92.3,92.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:95.2,95.66 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:98.69,99.13 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:100.17,101.42 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:102.27,103.38 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:104.15,105.52 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:106.10,107.61 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:111.66,118.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:123.36,127.16 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:127.16,129.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:131.2,134.9 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:134.9,136.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:138.2,141.19 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:141.19,144.3 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:146.2,146.78 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:146.78,150.3 3 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:152.2,152.24 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:152.24,154.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:154.17,156.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:157.3,157.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:157.23,160.4 2 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:161.3,161.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:166.2,166.21 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:166.21,168.46 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:168.46,171.4 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:174.2,174.16 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:178.102,185.18 4 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:185.18,186.31 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:186.31,188.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:189.3,189.21 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:193.2,193.27 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:193.27,196.17 3 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:196.17,198.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:198.9,198.22 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:198.22,199.36 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:199.36,200.13 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:202.4,202.67 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:202.67,203.13 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:205.4,205.19 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/deprecated/configencoding.go:209.2,209.20 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/auth.go:11.53,15.2 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/builder.go:13.53,22.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/buildx.go:33.46,36.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/buildx.go:39.54,46.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/buildx.go:46.16,48.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/buildx.go:49.2,56.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/buildx.go:60.44,76.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/buildx.go:79.69,84.2 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:39.42,40.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:40.14,42.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:43.2,44.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:44.17,46.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:47.2,47.20 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:47.20,49.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:50.2,50.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:53.54,56.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:62.57,64.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:66.45,67.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:67.14,69.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:70.2,70.51 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:78.52,92.2 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:94.39,95.18 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:95.18,97.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:98.2,99.28 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:99.28,101.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:102.2,102.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:111.62,138.2 6 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:140.50,141.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:141.14,143.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:144.2,145.19 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:145.19,147.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:148.2,148.19 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:148.19,150.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:151.2,151.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:151.17,153.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:154.2,154.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:163.46,164.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:164.14,166.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:167.2,168.18 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:168.18,170.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:171.2,171.24 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:171.24,173.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:174.2,174.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:174.29,176.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:177.2,177.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:180.58,184.2 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:193.44,194.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:194.14,196.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:197.2,197.77 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:213.51,249.2 14 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:251.39,252.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:252.14,254.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:255.2,256.20 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:256.20,258.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:259.2,259.18 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:259.18,261.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:262.2,262.25 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:262.25,264.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:265.2,265.25 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:265.25,267.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:268.2,268.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:268.29,270.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:271.2,271.27 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:271.27,273.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:274.2,274.25 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:274.25,276.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:277.2,277.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:277.29,279.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:280.2,280.26 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:280.26,282.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:284.2,284.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:291.53,296.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:298.40,299.18 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:299.18,301.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:302.2,302.39 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:309.60,312.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:314.47,315.26 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:315.26,317.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:318.2,318.55 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:328.37,329.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:329.14,331.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:332.2,332.70 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:337.30,339.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:352.54,380.2 7 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:382.41,383.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:383.16,385.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:386.2,386.64 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:391.41,392.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:392.14,394.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:395.2,395.22 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:406.52,410.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:412.40,413.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:413.14,415.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:416.2,420.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:432.43,433.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:433.14,435.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:436.2,442.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:451.60,459.2 6 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:461.47,462.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:462.29,464.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:465.2,466.25 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:466.25,468.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:469.2,469.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:469.28,471.14 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:471.14,473.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:474.3,474.65 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:476.2,476.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:476.31,478.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:479.2,479.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:489.48,490.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:490.14,492.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:493.2,493.81 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:508.52,541.2 8 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:543.39,544.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:544.16,546.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:547.2,547.74 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:557.56,568.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:578.68,584.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:588.49,590.23 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:590.23,592.14 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:592.14,593.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:595.3,595.27 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:597.2,597.35 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cache.go:600.38,602.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:31.62,33.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:33.16,35.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:55.2,56.28 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:56.28,58.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:60.2,60.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:60.31,67.3 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:80.2,100.16 6 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:100.16,102.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:104.2,114.21 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:177.32,179.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:181.34,183.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:185.31,187.2 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:190.41,192.28 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:192.28,197.3 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:198.2,200.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:205.42,207.15 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:207.15,209.35 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:209.35,211.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:214.2,215.15 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:215.15,217.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:218.2,218.30 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:222.29,225.19 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:225.19,227.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:228.2,228.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:231.76,235.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:235.16,237.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:238.2,243.16 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:243.16,245.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:251.2,252.52 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:252.52,257.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:260.2,263.16 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:263.16,265.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:266.2,298.35 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:298.35,300.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:301.2,301.24 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:301.24,303.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:304.2,304.35 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:304.35,306.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:307.2,307.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:307.33,309.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:310.2,310.21 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:310.21,312.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:313.2,313.21 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:313.21,315.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:316.2,316.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:316.33,318.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:319.2,319.36 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:319.36,321.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:322.2,322.39 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:322.39,324.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:325.2,325.32 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:325.32,327.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:328.2,328.18 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:328.18,330.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:331.2,331.35 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:331.35,333.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:334.2,334.15 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:334.15,336.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:337.2,337.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:337.31,339.25 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:339.25,341.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:342.3,342.34 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:344.2,344.30 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:344.30,346.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:347.2,347.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:347.23,349.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:350.2,350.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:350.31,352.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:353.2,353.32 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:353.32,356.3 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:357.2,357.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:357.28,359.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:361.2,361.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:361.33,367.3 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:369.2,370.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:370.16,372.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:376.2,377.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:377.16,379.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:380.2,381.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:381.16,383.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:384.2,386.16 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:386.16,388.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:389.2,390.24 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:390.24,391.25 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:392.15,393.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:394.11,396.18 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:396.18,397.13 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:399.4,399.25 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:403.2,404.18 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:404.18,406.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:407.2,409.8 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:414.60,417.20 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:417.20,419.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:420.2,425.16 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:425.16,427.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:428.2,428.21 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:428.21,430.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:432.2,445.16 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:445.16,447.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:448.2,455.18 6 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:467.54,469.22 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:469.22,471.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/cli.go:472.2,472.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:71.63,101.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:101.16,103.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:105.2,106.40 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:106.40,108.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:109.2,111.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:111.16,113.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:118.2,120.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:193.45,212.24 5 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:212.24,214.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:231.2,232.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:232.16,234.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:235.2,242.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:242.16,244.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:246.2,247.35 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:247.35,252.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:253.2,254.33 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:254.33,259.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:260.2,261.33 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:261.33,267.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:268.2,280.39 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:280.39,282.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:282.17,284.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:285.3,286.53 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:289.2,290.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:290.16,292.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:294.2,295.18 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:295.18,297.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:298.2,339.16 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:339.16,341.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:343.2,343.49 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:343.49,345.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:346.2,346.39 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:346.39,349.30 3 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:349.30,351.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:352.3,352.37 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:355.2,355.21 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:359.47,361.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:363.100,441.16 10 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:441.16,443.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:445.2,445.11 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:445.11,447.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:449.2,453.16 5 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:453.16,455.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:456.2,456.12 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:459.84,472.16 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:472.16,474.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:476.2,477.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:477.16,479.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:481.2,481.46 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:547.73,561.56 8 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:561.56,563.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:567.2,567.12 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:571.94,573.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:573.16,575.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:578.2,579.85 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:579.85,581.21 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:581.21,583.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:584.3,584.49 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:597.2,603.80 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:603.80,607.3 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:609.2,609.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:613.86,617.2 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:619.62,621.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:621.16,623.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:624.2,624.66 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:624.66,626.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/client.go:627.2,627.22 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:40.49,42.23 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:42.23,44.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:45.2,45.10 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:48.47,59.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:61.53,70.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:77.9,78.22 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:78.22,80.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:81.2,81.66 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:81.66,83.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:85.2,86.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:86.16,88.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:89.2,91.36 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:91.36,94.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:95.2,95.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:95.16,97.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:99.2,102.12 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:109.105,115.68 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:115.68,117.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:117.17,119.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:120.3,120.15 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:123.2,123.37 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:123.37,125.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:125.17,127.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:130.2,130.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:130.33,133.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:133.17,135.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:136.3,136.44 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:136.44,138.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:142.2,142.45 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:142.45,143.35 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:143.35,145.18 2 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:145.18,147.5 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:148.4,148.45 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:148.45,150.5 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:154.2,154.44 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:158.70,160.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:162.58,163.102 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:163.102,164.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:164.17,166.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:167.3,167.18 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:167.18,169.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:172.3,173.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:173.17,175.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:176.3,176.46 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:178.2,178.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:178.16,180.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:182.2,182.44 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:185.53,190.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:190.16,192.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:193.2,194.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:194.16,196.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:197.2,197.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:205.91,211.33 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:211.33,214.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:218.2,218.26 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:218.26,220.44 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:220.44,221.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:223.3,223.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:223.17,225.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:226.3,229.17 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:229.17,231.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:232.3,232.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:235.2,235.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:238.48,241.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:243.49,246.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:250.64,252.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:252.16,254.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/context.go:255.2,255.77 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/dedent.go:9.30,13.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/dockerfile.go:12.50,29.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:48.51,79.2 9 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:81.38,82.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:82.16,84.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:85.2,85.87 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:88.36,89.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:89.17,91.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:91.17,93.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:94.3,94.40 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:96.2,96.23 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:96.23,98.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:99.2,99.20 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:99.20,101.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:102.2,102.14 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:107.43,108.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:108.14,110.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:111.2,111.25 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:124.52,129.2 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:131.40,132.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:132.14,134.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:135.2,136.18 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:136.18,138.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:139.2,139.18 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:139.18,141.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:143.2,150.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:157.49,160.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:162.37,163.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:163.14,165.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:166.2,166.79 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:184.51,213.2 8 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:215.39,216.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:216.14,218.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:219.2,220.19 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:220.19,222.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:223.2,223.27 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:223.27,225.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:226.2,226.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:226.23,228.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:229.2,229.32 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:229.32,231.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:232.2,232.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:232.28,234.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:235.2,235.21 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:235.21,237.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:238.2,238.20 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:238.20,240.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:241.2,248.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:255.54,257.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:259.42,260.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:260.14,262.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:263.2,263.82 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:270.39,271.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:271.14,273.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:274.2,274.50 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:277.51,279.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:285.37,286.14 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:286.14,288.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:289.2,289.48 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:296.53,299.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:301.40,302.18 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:302.18,304.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:305.2,305.49 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:314.61,322.2 6 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:324.48,325.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:325.29,327.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:328.2,329.25 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:329.25,331.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:332.2,332.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:332.28,334.14 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:334.14,336.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:337.3,337.65 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:339.2,339.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:339.31,341.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:342.2,342.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:349.42,351.28 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:351.28,353.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:354.2,354.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:357.55,359.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:365.48,367.34 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:367.34,369.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:370.2,371.33 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:374.61,378.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/export.go:380.61,382.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:24.45,26.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:26.16,28.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:30.2,31.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:31.16,33.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:34.2,40.15 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:49.64,55.43 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:55.43,57.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:59.2,60.70 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:60.70,62.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:63.2,67.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:67.16,69.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:74.2,74.42 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:74.42,76.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:76.17,78.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:79.3,82.17 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:82.17,84.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:85.2,86.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:86.31,87.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:87.23,88.13 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:90.4,90.40 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:90.40,91.13 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:93.4,93.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:93.23,94.13 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:96.4,97.18 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:97.18,98.13 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:100.4,100.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:100.28,101.24 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:101.24,102.26 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:104.5,104.66 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:104.66,105.26 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:111.4,112.9 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:119.2,120.39 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:120.39,122.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/host.go:124.2,127.20 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:62.45,83.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:112.50,285.2 23 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:297.51,328.2 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:330.96,333.55 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:333.55,335.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:337.2,344.33 4 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:354.47,356.38 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:356.38,358.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:361.2,363.58 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:363.58,365.26 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:365.26,366.38 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:366.38,368.5 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:391.2,391.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:398.39,400.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:402.63,404.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:406.62,432.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:434.39,438.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:441.40,442.13 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:442.13,444.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:445.2,445.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:445.31,446.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:446.17,448.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:450.2,450.14 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:453.50,454.30 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:454.30,456.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:457.2,457.22 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:467.58,469.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:471.32,473.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:475.45,477.30 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:477.30,479.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:480.2,480.35 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:483.34,485.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:490.20,492.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:492.16,494.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:496.2,496.26 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:496.26,501.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:503.2,503.54 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:503.54,507.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:509.2,511.8 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:517.87,520.25 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:520.25,524.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:525.2,525.24 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:525.24,529.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:530.2,530.49 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:530.49,534.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:536.2,536.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:536.31,538.53 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:538.53,539.66 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:539.66,541.62 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:541.62,546.6 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:548.9,548.84 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:548.84,553.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:556.2,556.34 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:556.34,558.54 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:558.54,561.4 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:564.2,564.32 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:564.32,566.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:570.2,573.19 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:573.19,575.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:576.2,576.19 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:576.19,578.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:579.2,579.37 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:579.37,580.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:580.23,581.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:583.3,583.45 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:583.45,591.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:593.3,594.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:594.17,596.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:598.3,599.74 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:599.74,607.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:609.3,609.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:612.2,612.13 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:612.13,614.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:614.29,615.25 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:615.25,617.5 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:621.2,622.39 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:622.39,624.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:624.17,626.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:628.3,628.44 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:631.2,632.39 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:632.39,633.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:633.23,634.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:636.3,636.45 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:636.45,644.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:646.3,647.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:647.17,649.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:651.3,651.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:651.23,652.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:654.3,654.43 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:657.2,658.37 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:658.37,659.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:659.23,660.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:662.3,662.45 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:662.45,670.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:672.3,673.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:673.17,675.12 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:677.3,677.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:677.23,678.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:680.3,680.39 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:683.2,684.33 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:684.33,686.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:686.17,688.12 2 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:690.3,690.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:690.23,691.12 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:693.3,693.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:696.2,696.76 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:696.76,704.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:706.2,706.34 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:706.34,707.47 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:707.47,709.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:712.2,713.37 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:713.37,718.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:720.2,741.23 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:751.23,755.16 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:755.16,757.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:759.2,760.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:760.16,762.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:763.2,763.9 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:763.9,765.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:767.2,768.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:768.16,770.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:772.2,777.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:777.16,779.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:780.2,782.46 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:782.46,784.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:785.2,785.35 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:785.35,788.3 2 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:790.2,791.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:791.16,793.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:795.2,795.41 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:795.41,798.17 3 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:798.17,800.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:805.2,805.30 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:805.30,806.74 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:806.74,808.4 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:811.2,811.24 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:811.24,814.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:818.2,818.33 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:818.33,820.10 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:820.10,821.12 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:824.3,825.8 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:828.2,828.19 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:837.31,840.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:854.3,856.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:856.16,858.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:867.2,867.25 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:867.25,870.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:872.2,875.33 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:875.33,877.10 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:877.10,880.9 2 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:884.3,885.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:885.17,887.12 2 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:890.3,890.27 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:890.27,891.87 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:891.87,893.13 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:895.4,895.20 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:895.20,897.13 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:900.4,901.9 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:907.2,907.49 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:907.49,909.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:911.2,913.32 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:923.9,925.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:925.16,927.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:929.2,931.33 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:931.33,933.17 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:933.17,934.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:936.3,937.30 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:937.30,938.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:940.3,942.31 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:942.31,943.23 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:943.23,945.5 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:946.4,946.24 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:946.24,948.5 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:954.2,954.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:964.34,968.54 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:968.54,970.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:971.2,971.56 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:971.56,973.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:974.2,974.48 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:974.48,976.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:977.2,977.52 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:977.52,979.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:980.2,980.56 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:980.56,982.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:983.2,983.52 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:983.52,985.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:986.2,986.52 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:986.52,988.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:989.2,989.64 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:989.64,991.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:992.2,992.58 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:992.58,994.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:995.2,995.54 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:995.54,997.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:999.2,999.58 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:999.58,1001.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1002.2,1002.50 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1002.50,1004.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1005.2,1005.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1005.28,1007.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1008.2,1008.34 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1008.34,1010.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1011.2,1011.52 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1011.52,1013.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1014.2,1014.56 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1014.56,1016.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1017.2,1017.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1017.28,1019.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1020.2,1020.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1020.28,1022.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1023.2,1023.52 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1023.52,1025.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1026.2,1026.44 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1026.44,1028.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1029.2,1029.46 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1029.46,1031.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1032.2,1032.50 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1032.50,1034.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1038.2,1038.68 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1038.68,1040.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1043.2,1048.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1048.16,1050.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1051.2,1051.30 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1051.30,1053.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1056.2,1056.50 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1056.50,1058.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1058.8,1059.42 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1059.42,1061.74 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1061.74,1062.13 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1064.4,1065.9 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1069.2,1073.8 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1076.41,1078.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1078.16,1080.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1081.2,1082.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1082.16,1084.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1085.2,1085.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1091.51,1093.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1093.16,1095.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1096.2,1097.48 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1097.48,1099.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1101.2,1104.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1104.16,1106.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/image.go:1108.2,1108.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:40.46,50.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:52.50,71.2 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:78.32,81.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:89.24,93.16 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:93.16,95.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:97.2,97.13 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:97.13,99.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:103.2,104.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:104.16,106.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:108.2,109.19 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:117.44,122.16 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:122.16,124.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:132.2,133.84 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:133.84,136.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:137.2,137.85 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:137.85,140.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:141.2,141.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:141.16,143.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:145.2,145.49 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:145.49,147.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:149.2,149.32 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:157.48,159.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:159.16,161.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:176.2,176.74 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:176.74,184.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:186.2,186.35 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:186.35,187.61 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:187.61,195.4 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:198.2,198.28 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:201.82,203.16 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:203.16,205.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:206.2,208.70 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:208.70,210.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:211.2,211.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:219.34,223.26 3 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:223.26,225.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:226.2,226.52 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:226.52,228.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:229.2,229.52 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:229.52,231.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:232.2,232.54 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:232.54,234.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:237.2,241.8 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:248.19,251.55 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:251.55,253.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/images.go:255.2,259.33 4 1
github.com/pulumi/pulumi-docker/provider/v4/internal/network.go:15.60,30.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/platform.go:26.54,57.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/platform.go:59.35,61.2 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:14.49,16.26 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:16.26,17.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:17.17,18.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:20.3,20.29 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:22.2,22.15 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:28.43,29.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:29.16,31.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:32.2,32.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:39.43,40.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:40.16,42.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:43.2,43.41 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:52.51,53.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:53.16,55.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:56.2,56.44 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:62.64,63.31 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:63.31,65.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:66.2,68.26 3 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:68.26,69.20 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:69.20,70.12 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:72.3,72.20 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:72.20,73.12 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:75.3,75.22 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:77.2,77.17 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:82.59,83.38 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:83.38,85.3 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:87.2,89.29 3 0
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:89.29,90.42 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:90.42,91.12 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:93.3,93.15 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/preview.go:96.2,99.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/ssh.go:14.43,31.2 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/ssh.go:33.30,34.16 1 1
github.com/pulumi/pulumi-docker/provider/v4/internal/ssh.go:34.16,36.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/ssh.go:38.2,40.22 2 1
github.com/pulumi/pulumi-docker/provider/v4/internal/ssh.go:40.22,42.3 1 0
github.com/pulumi/pulumi-docker/provider/v4/internal/ssh.go:44.2,44.10 1 1

View File

@@ -0,0 +1,27 @@
// 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 (
"strings"
dd "github.com/muesli/reflow/dedent"
)
func dedent(s string) string {
return strings.TrimSpace(dd.String(
strings.ReplaceAll(s, `"`, "`"),
))
}

View File

@@ -0,0 +1,51 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDedent(t *testing.T) {
t.Parallel()
tests := []struct {
name string
given string
want string
}{
{
name: "simple case",
given: `
An optional map of named build-time argument variables to set during
the Docker build. This flag allows you to pass build-time variables that
can be accessed like environment variables inside the "RUN"
instruction.`,
want: `An optional map of named build-time argument variables to set during
the Docker build. This flag allows you to pass build-time variables that
can be accessed like environment variables inside the ` + "`RUN`\n" + `instruction.`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := dedent(tt.given)
assert.Equal(t, tt.want, actual)
})
}
}

View File

@@ -0,0 +1,213 @@
// 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 deprecated
import (
"encoding/json"
"fmt"
"sort"
"google.golang.org/protobuf/types/known/structpb"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
)
// ConfigEncoding handles unmarshaling legacy JSON provider config.
type ConfigEncoding struct {
schema schema.ConfigSpec
}
// New constructs a new config encoder for the provided spec.
func New(s schema.ConfigSpec) *ConfigEncoding {
return &ConfigEncoding{schema: s}
}
func (*ConfigEncoding) tryUnwrapSecret(encoded any) (any, bool) {
m, ok := encoded.(map[string]any)
if !ok {
return nil, false
}
sig, ok := m["4dabf18193072939515e22adb298388d"]
if !ok {
return nil, false
}
ss, ok := sig.(string)
if !ok {
return nil, false
}
if ss != "1b47061264138c4ac30d75fd1eb44270" {
return nil, false
}
value, ok := m["value"]
return value, ok
}
func (enc *ConfigEncoding) convertStringToPropertyValue(s string, prop schema.PropertySpec) (
resource.PropertyValue, error,
) {
// If the schema expects a string, we can just return this as-is.
if prop.Type == "string" {
return resource.NewStringProperty(s), nil
}
// Otherwise, we will attempt to deserialize the input string as JSON and convert the result into a Pulumi
// property. If the input string is empty, we will return an appropriate zero value.
if s == "" {
return enc.zeroValue(prop.Type), nil
}
var jsonValue interface{}
if err := json.Unmarshal([]byte(s), &jsonValue); err != nil {
return resource.PropertyValue{}, err
}
opts := enc.unmarshalOpts()
// Instead of using resource.NewPropertyValue, specialize it to detect nested json-encoded secrets.
var replv func(encoded any) (resource.PropertyValue, bool)
replv = func(encoded any) (resource.PropertyValue, bool) {
encodedSecret, isSecret := enc.tryUnwrapSecret(encoded)
if !isSecret {
return resource.NewNullProperty(), false
}
v := resource.NewPropertyValueRepl(encodedSecret, nil, replv)
if opts.KeepSecrets {
v = resource.MakeSecret(v)
}
return v, true
}
return resource.NewPropertyValueRepl(jsonValue, nil, replv), nil
}
func (*ConfigEncoding) zeroValue(typ string) resource.PropertyValue {
switch typ {
case "boolean":
return resource.NewPropertyValue(false)
case "integer", "number":
return resource.NewPropertyValue(0)
case "array":
return resource.NewPropertyValue([]interface{}{})
default:
return resource.NewPropertyValue(map[string]interface{}{})
}
}
func (enc *ConfigEncoding) unmarshalOpts() plugin.MarshalOptions {
return plugin.MarshalOptions{
Label: "config",
KeepUnknowns: true,
SkipNulls: true,
RejectAssets: true,
}
}
// Like plugin.UnmarshalPropertyValue but overrides string parsing with convertStringToPropertyValue.
func (enc *ConfigEncoding) unmarshalPropertyValue(key resource.PropertyKey,
v *structpb.Value,
) (*resource.PropertyValue, error) {
opts := enc.unmarshalOpts()
pv, err := plugin.UnmarshalPropertyValue(key, v, enc.unmarshalOpts())
if err != nil {
return nil, fmt.Errorf("error unmarshalling property %q: %w", key, err)
}
prop, ok := enc.schema.Variables[string(key)]
// Only apply JSON-encoded recognition for known fields.
if !ok {
return pv, nil
}
var jsonString string
var jsonStringDetected, jsonStringSecret bool
if pv.IsString() {
jsonString = pv.StringValue()
jsonStringDetected = true
}
if opts.KeepSecrets && pv.IsSecret() && pv.SecretValue().Element.IsString() {
jsonString = pv.SecretValue().Element.StringValue()
jsonStringDetected = true
jsonStringSecret = true
}
if jsonStringDetected {
v, err := enc.convertStringToPropertyValue(jsonString, prop)
if err != nil {
return nil, fmt.Errorf("error unmarshalling property %q: %w", key, err)
}
if jsonStringSecret {
s := resource.MakeSecret(v)
return &s, nil
}
return &v, nil
}
// Computed sentinels are coming in as always having an empty string, but the encoding coerses them to a zero
// value of the appropriate type.
if pv.IsComputed() {
el := pv.V.(resource.Computed).Element
if el.IsString() && el.StringValue() == "" {
res := resource.MakeComputed(enc.zeroValue(prop.Type))
return &res, nil
}
}
return pv, nil
}
// UnmarshalProperties is copied from plugin.UnmarshalProperties substituting plugin.UnmarshalPropertyValue.
func (enc *ConfigEncoding) UnmarshalProperties(
props *structpb.Struct,
) (resource.PropertyMap, error) {
opts := enc.unmarshalOpts()
result := make(resource.PropertyMap)
// First sort the keys so we enumerate them in order (in case errors happen, we want determinism).
var keys []string
if props != nil {
for k := range props.Fields {
keys = append(keys, k)
}
sort.Strings(keys)
}
// And now unmarshal every field it into the map.
for _, key := range keys {
pk := resource.PropertyKey(key)
v, err := enc.unmarshalPropertyValue(pk, props.Fields[key])
if err != nil {
return nil, err
} else if v != nil {
if opts.SkipNulls && v.IsNull() {
continue
}
if opts.SkipInternalKeys && resource.IsInternalPropertyKey(pk) {
continue
}
result[pk] = *v
}
}
return result, nil
}

View File

@@ -0,0 +1,295 @@
// 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 deprecated
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
structpb "google.golang.org/protobuf/types/known/structpb"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
)
func TestConfigEncoding(t *testing.T) {
t.Parallel()
type testCase struct {
ty schema.TypeSpec
v *structpb.Value
pv resource.PropertyValue
}
knownKey := "mykey"
makeEnc := func(typ schema.TypeSpec) *ConfigEncoding {
return New(
schema.ConfigSpec{
Variables: map[string]schema.PropertySpec{
knownKey: {
TypeSpec: typ,
},
},
},
)
}
makeValue := func(x any) *structpb.Value {
vv, err := structpb.NewValue(x)
assert.NoErrorf(t, err, "structpb.NewValue failed")
return vv
}
checkUnmarshal := func(t *testing.T, tc testCase) {
enc := makeEnc(tc.ty)
pv, err := enc.unmarshalPropertyValue(resource.PropertyKey(knownKey), tc.v)
assert.NoError(t, err)
assert.NotNil(t, pv)
assert.Equal(t, tc.pv, *pv)
}
turnaroundTestCases := []testCase{
{
schema.TypeSpec{Type: "boolean"},
makeValue(`true`),
resource.NewBoolProperty(true),
},
{
schema.TypeSpec{Type: "boolean"},
makeValue(`false`),
resource.NewBoolProperty(false),
},
{
schema.TypeSpec{Type: "integer"},
makeValue(`0`),
resource.NewNumberProperty(0),
},
{
schema.TypeSpec{Type: "integer"},
makeValue(`42`),
resource.NewNumberProperty(42),
},
{
schema.TypeSpec{Type: "number"},
makeValue(`0`),
resource.NewNumberProperty(0.0),
},
{
schema.TypeSpec{Type: "number"},
makeValue(`42.5`),
resource.NewNumberProperty(42.5),
},
{
schema.TypeSpec{Type: "string"},
structpb.NewStringValue(""),
resource.NewStringProperty(""),
},
{
schema.TypeSpec{Type: "string"},
structpb.NewStringValue("hello"),
resource.NewStringProperty("hello"),
},
{
schema.TypeSpec{Type: "array"},
makeValue(`[]`),
resource.NewArrayProperty([]resource.PropertyValue{}),
},
{
schema.TypeSpec{Type: "array"},
makeValue(`["hello","there"]`),
resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty("hello"),
resource.NewStringProperty("there"),
}),
},
{
schema.TypeSpec{Type: "object"},
makeValue(`{}`),
resource.NewObjectProperty(resource.PropertyMap{}),
},
{
schema.TypeSpec{Type: "object"},
makeValue(`{"key":"value"}`),
resource.NewObjectProperty(resource.PropertyMap{
"key": resource.NewStringProperty("value"),
}),
},
}
t.Run("turnaround", func(t *testing.T) {
for i, tc := range turnaroundTestCases {
tc := tc
t.Run(fmt.Sprintf("UnmarshalPropertyValue/%d", i), func(t *testing.T) {
t.Parallel()
checkUnmarshal(t, tc)
})
}
})
t.Run("zero_values", func(t *testing.T) {
// Historically the encoding was able to convert empty strings into type-appropriate zero values.
cases := []testCase{
{
schema.TypeSpec{Type: "boolean"},
makeValue(""),
resource.NewBoolProperty(false),
},
{
schema.TypeSpec{Type: "number"},
makeValue(""),
resource.NewNumberProperty(0.),
},
{
schema.TypeSpec{Type: "integer"},
makeValue(""),
resource.NewNumberProperty(0),
},
{
schema.TypeSpec{Type: "string"},
makeValue(""),
resource.NewStringProperty(""),
},
{
schema.TypeSpec{Type: "object"},
makeValue(""),
resource.NewObjectProperty(make(resource.PropertyMap)),
},
{
schema.TypeSpec{Type: "array"},
makeValue(""),
resource.NewArrayProperty([]resource.PropertyValue{}),
},
}
for _, tc := range cases {
tc := tc
t.Run(fmt.Sprintf("%v", tc.ty), func(t *testing.T) {
t.Parallel()
checkUnmarshal(t, tc)
})
}
})
t.Run("computed", func(t *testing.T) {
unk := makeValue(plugin.UnknownStringValue)
for i, tc := range turnaroundTestCases {
tc := tc
t.Run(fmt.Sprintf("UnmarshalPropertyValue/%d", i), func(t *testing.T) {
t.Parallel()
// Unknown sentinel unmarshals to a Computed with a type-appropriate zero value.
checkUnmarshal(t, testCase{
tc.ty,
unk,
resource.MakeComputed(makeEnc(tc.ty).zeroValue(tc.ty.Type)),
})
})
}
})
t.Run("secret", func(t *testing.T) {
// Unmarshalling happens with KeepSecrets=false, replacing them with the underlying values. This case
// does not need to be tested.
//
// Marhalling however supports sending secrets back to the engine, intending to mark values as secret
// that happen on paths that are declared as secret in the schema. Due to the limitation of the
// JSON-in-proto-encoding, secrets are communicated imprecisely as an approximation: if any nested
// element of a property is secret, the entire property is marshalled as secret.
var secretCases []testCase
pbSecret := func(v *structpb.Value) *structpb.Value {
return structpb.NewStructValue(&structpb.Struct{Fields: map[string]*structpb.Value{
"4dabf18193072939515e22adb298388d": makeValue("1b47061264138c4ac30d75fd1eb44270"),
"value": v,
}})
}
for _, tc := range turnaroundTestCases {
secretCases = append(secretCases, testCase{
tc.ty,
pbSecret(tc.v),
resource.MakeSecret(tc.pv),
})
}
for i, tc := range secretCases {
tc := tc
t.Run(fmt.Sprintf("secret/UnmarshalPropertyValue/%d", i), func(t *testing.T) {
t.Parallel()
// Unmarshallin will remove secrts, so the expected value needs to be modified.
tc.pv = tc.pv.SecretValue().Element
checkUnmarshal(t, tc)
})
}
t.Run("tolerate secrets in Configure", func(t *testing.T) {
// This is a bit of a histirocal quirk: the engine may send secrets to Configure before
// receiving the response from Configure indicating that the provider does not want to receive
// secrets. These are simply ignored. The engine does not currently send secrets to CheckConfig.
// The engine does take care of making sure the secrets are stored as such in the statefile.
//
// Check here that unmarshalilng such values removes the secrets.
checkUnmarshal(t, testCase{
schema.TypeSpec{Type: "object"},
pbSecret(makeValue(`{"key":"val"}`)),
resource.NewObjectProperty(resource.PropertyMap{
"key": resource.NewStringProperty("val"),
}),
})
})
})
regressUnmarshalTestCases := []testCase{
{
schema.TypeSpec{Type: "array"},
makeValue(`
[
{
"address": "somewhere.org",
"password": {
"4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
"value": "some-password"
},
"username": "some-user"
}
]`),
resource.NewArrayProperty([]resource.PropertyValue{
resource.NewObjectProperty(resource.PropertyMap{
"address": resource.NewStringProperty("somewhere.org"),
"password": resource.NewStringProperty("some-password"),
"username": resource.NewStringProperty("some-user"),
}),
}),
},
}
t.Run("regress-unmarshal", func(t *testing.T) {
for i, tc := range regressUnmarshalTestCases {
tc := tc
t.Run(fmt.Sprintf("UnmarshalPropertyValue/%d", i), func(t *testing.T) {
t.Parallel()
checkUnmarshal(t, tc)
})
}
})
}

View File

@@ -0,0 +1,19 @@
// 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 deprecated vendors config parsing from pulumi-terraform-bridge.
//
// Originally taken from here:
// https://github.com/pulumi/pulumi-terraform-bridge/blob/90733a0c7/pkg/tfbridge/config_encoding.go
package deprecated

63
provider/internal/doc.go Normal file
View File

@@ -0,0 +1,63 @@
// 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 contains our clients, validation, and provider
// implementation for interacting with Docker's buildx APIs.
//
// The provider has two primary modes of operation when building an image. The
// default behavior is to use an embedded Docker CLI, which does not require to
// actually be installed on a host in order to perform builds (a build daemon
// must still be accessible locally or remotely). The second mode execs a
// "docker-buildx" binary on the host to perform builds. This second mode was
// added primarily for compatibility with Docker Build Cloud, which requires a
// custom docker-buildx binary.
//
// # CLIs
//
// In both execution modes we have several CLI clients. The first client is
// scoped to the host and initialized as part of the provider's Configure call.
// We use this CLI for host-level operations, in particular when we potentially
// initialize a new buildx builder and when we fetch existing credentials on
// the host.
//
// Each operation then has a CLI instance scoped to the life of the operation.
// This allows us to layer resource-scoped credentials on top of the host's
// existing credentials, and in practice Docker seems to handle these
// connections more reliably than a single CLI for all operations.
//
// # Credentials
//
// When using the embedded Docker client, secrets are communicated to the build
// daemon natively via gRPC callbacks. When running in exec mode, credentials
// must be communicated to the buildx binary via a configuration file. In order
// to not pollute the host's existing credentials with e.g. short-lived ECR
// tokens, we copy a small subset of the host's Docker config to a temporary
// directory and use that for the lifetime of the exec operation.
//
// # Preview mode
//
// The pulumi-go-provider primarily operates on simple Go structs and doesn't
// currently have a way to distinguish whether a value is unknown or empty. We
// ignore anything that is a zero value during previews before we apply
// validation or perform builds.
//
// # Diffs
//
// Another limitation of pulumi-go-provider is that it doesn't currently allow
// us to override the default Diff behavior. We intentionally apply
// "ignoreChanges" semantics to registry passwords, in order to reduce noise
// and unnecessary updates, but as a result we have to re-implement Diff from
// the ground up. This implementation is not nearly as rich as the default
// experience and should be replaced when an alternative is available.
package internal

View File

@@ -0,0 +1,108 @@
// 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 (
"errors"
"io"
"os"
"path/filepath"
"strings"
buildx "github.com/docker/buildx/build"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/pulumi/pulumi-go-provider/infer"
)
// Dockerfile references a local, remote, or inline Dockerfile.
type Dockerfile struct {
Location string `pulumi:"location,optional"`
Inline string `pulumi:"inline,optional"`
}
// Annotate sets docstrings on Dockerfile.
func (d *Dockerfile) Annotate(a infer.Annotator) {
a.Describe(&d.Location, dedent(`
Location of the Dockerfile to use.
Can be a relative or absolute path to a local file, or a remote URL.
Defaults to "${context.location}/Dockerfile" if context is on-disk.
Conflicts with "inline".
`))
a.Describe(&d.Inline, dedent(`
Raw Dockerfile contents.
Conflicts with "location".
Equivalent to invoking Docker with "-f -".
`))
}
func (d *Dockerfile) validate(preview bool, c *Context) error {
if d.Location != "" && d.Inline != "" {
return newCheckFailure(
errors.New(`only specify "file" or "inline", not both`),
"dockerfile",
)
}
if d.Location != "" {
if buildx.IsRemoteURL(d.Location) {
return nil
}
abs, err := filepath.Abs(d.Location)
if err != nil {
return err
}
f, err := os.Open(filepath.Clean(abs))
if err != nil {
return newCheckFailure(err, "dockerfile.location")
}
if err := parseDockerfile(f); err != nil {
return newCheckFailure(err, "dockerfile.location")
}
return nil
}
if d.Inline != "" {
err := parseDockerfile(strings.NewReader(d.Inline))
if err != nil {
return newCheckFailure(err, "dockerfile.inline")
}
return nil
}
if !preview && c != nil && !buildx.IsRemoteURL(c.Location) {
return newCheckFailure(errors.New("missing 'location' or 'inline'"), "dockerfile")
}
return nil
}
func parseDockerfile(r io.Reader) error {
parsed, err := parser.Parse(r)
if err != nil {
return newCheckFailure(err, "dockerfile")
}
_, _, err = instructions.Parse(parsed.AST)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,102 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateDockerfile(t *testing.T) {
t.Parallel()
tests := []struct {
name string
d Dockerfile
givenC Context
preview bool
wantErr string
}{
{
name: "relative",
d: Dockerfile{
Location: "../internal/../internal/testdata/noop/Dockerfile",
},
},
{
name: "missing file",
d: Dockerfile{
Location: "/does/not/exist/Dockerfile",
},
wantErr: "no such file",
},
{
name: "invalid syntax",
d: Dockerfile{
Location: "testdata/Dockerfile.invalid",
},
wantErr: "unknown instruction: RUNN",
},
{
name: "invalid syntax inline",
d: Dockerfile{
Inline: "RUNN it",
},
wantErr: "unknown instruction: RUNN",
},
{
name: "valid syntax inline",
d: Dockerfile{
Inline: "FROM scratch",
},
},
{
name: "unset",
d: Dockerfile{},
wantErr: "missing 'location' or 'inline'",
},
{
name: "unset with remote context",
d: Dockerfile{},
givenC: Context{Location: "https://github.com/foobar"},
},
{
name: "preview",
d: Dockerfile{},
preview: true,
},
{
name: "over-specified",
d: Dockerfile{Location: ".", Inline: "FROM scratch"},
wantErr: `only specify "file" or "inline", not both`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.d.validate(tt.preview, &tt.givenC)
if tt.wantErr == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.wantErr)
}
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,224 @@
## Migrating Pulumi Docker v3 and v4 Image resources
This provider's `Image` resource provides a superset of functionality over the `Image` resources available in versions 3 and 4 of the Pulumi Docker provider.
Existing `Image` resources can be converted to the docker-build `Image` resources with minor modifications.
### Behavioral differences
There are several key behavioral differences to keep in mind when transitioning images to the new `Image` resource.
#### Previews
Version `3.x` of the Pulumi Docker provider always builds images during preview operations.
This is helpful as a safeguard to prevent "broken" images from merging, but users found the behavior unnecessarily redundant when running previews and updates locally.
Version `4.x` changed build-on-preview behavior to be opt-in.
By default, `v4.x` `Image` resources do _not_ build during previews, but this behavior can be toggled with the `buildOnPreview` option.
Several users reported outages due to the default behavior allowing bad images to accidentally sneak through CI.
The default behavior of this provider's `Image` resource is similar to `3.x` and will build images during previews.
This behavior can be changed by specifying `buildOnPreview`.
#### Push behavior
Versions `3.x` and `4.x` of the Pulumi Docker provider attempt to push images to remote registries by default.
They expose a `skipPush: true` option to disable pushing.
This provider's `Image` resource matches the Docker CLI's behavior and does not push images anywhere by default.
To push images to a registry you can include `push: true` (equivalent to Docker's `--push` flag) or configure an `export` of type `registry` (equivalent to Docker's `--output type=registry`).
Like Docker, if an image is configured without exports you will see a warning with instructions for how to enable pushing, but the build will still proceed normally.
#### Secrets
Version `3.x` of the Pulumi Docker provider supports secrets by way of the `extraOptions` field.
Version `4.x` of the Pulumi Docker provider does not support secrets.
The `Image` resource supports secrets but does not require those secrets to exist on-disk or in environment variables.
Instead, they should be passed directly as values.
(Please be sure to familiarize yourself with Pulumi's [native secret handling](https://www.pulumi.com/docs/concepts/secrets/).)
Pulumi also provides [ESC](https://www.pulumi.com/product/esc/) to make it easier to share secrets across stacks and environments.
#### Caching
Version `3.x` of the Pulumi Docker provider exposes `cacheFrom: bool | { stages: [...] }`.
It builds targets individually and pushes them to separate images for caching.
Version `4.x` exposes a similar parameter `cacheFrom: { images: [...] }` which pushes and pulls inline caches.
Both versions 3 and 4 require specific environment variables to be set and deviate from Docker's native caching behavior.
This can result in inefficient builds due to unnecessary image pulls, repeated file transfers, etc.
The `Image` resource delegates all caching behavior to Docker.
`cacheFrom` and `cacheTo` options (equivalent to Docker's `--cache-to` and `--cache-from`) are exposed and provide additional cache targets, such as local disk, S3 storage, etc.
#### Outputs
Versions `3.x` and `4.x` of the provider exposed a `repoDigest` output which was a fully qualified tag with digest.
In `4.x` this could also be a single sha256 hash if the image wasn't pushed.
Unlike earlier providers the `Image` resource can push multiple tags.
As a convenience, it exposes a `ref` output consisting of a tag with digest as long as the image was pushed.
If multiple tags were pushed this uses one at random.
If you need more control over tag references you can use the `digest` output, which is always a single sha256 hash as long as the image was exported somewhere.
#### Tag deletion and refreshes
Versions 3 and 4 of Pulumi Docker provider do not delete tags when the `Image` resource is deleted, nor do they confirm expected tags exist during `refresh` operations.
The `buidx.Image` will query your registries during `refresh` to ensure the expected tags exist.
If any are missing a subsequent `update` will push them.
When a `Image` is deleted, it will _attempt_ to also delete any pushed tags.
Deletion of remote tags is not guaranteed because not all registries support the manifest `DELETE` API (`docker.io` in particular).
Manifests are _not_ deleted in the same way during updates -- to do so safely would require a full build to determine whether a Pulumi operation should be an update or update-replace.
Use the [`retainOnDelete: true`](https://www.pulumi.com/docs/concepts/options/retainondelete/) option if you do not want tags deleted.
### Example migration
Examples of "fully-featured" `v3` and `v4` `Image` resources are shown below, along with an example `Image` resource showing how they would look after migration.
The `v3` resource leverages `buildx` via a `DOCKER_BUILDKIT` environment variable and CLI flags passed in with `extraOption`.
After migration, the environment variable is no longer needed and CLI flags are now properties on the `Image`.
In almost all cases, properties of `Image` are named after the Docker CLI flag they correspond to.
The `v4` resource is less functional than its `v3` counterpart because it lacks the flexibility of `extraOptions`.
It it is shown with parameters similar to the `v3` example for completeness.
{{% examples %}}
## Example Usage
{{% example %}}
### v3/v4 migration
```typescript
// v3 Image
const v3 = new docker.Image("v3-image", {
imageName: "myregistry.com/user/repo:latest",
localImageName: "local-tag",
skipPush: false,
build: {
dockerfile: "./Dockerfile",
context: "../app",
target: "mytarget",
args: {
MY_BUILD_ARG: "foo",
},
env: {
DOCKER_BUILDKIT: "1",
},
extraOptions: [
"--cache-from",
"type=registry,myregistry.com/user/repo:cache",
"--cache-to",
"type=registry,myregistry.com/user/repo:cache",
"--add-host",
"metadata.google.internal:169.254.169.254",
"--secret",
"id=mysecret,src=/local/secret",
"--ssh",
"default=/home/runner/.ssh/id_ed25519",
"--network",
"host",
"--platform",
"linux/amd64",
],
},
registry: {
server: "myregistry.com",
username: "username",
password: pulumi.secret("password"),
},
});
// v3 Image after migrating to docker-build.Image
const v3Migrated = new dockerbuild.Image("v3-to-buildx", {
tags: ["myregistry.com/user/repo:latest", "local-tag"],
push: true,
dockerfile: {
location: "./Dockerfile",
},
context: {
location: "../app",
},
target: "mytarget",
buildArgs: {
MY_BUILD_ARG: "foo",
},
cacheFrom: [{ registry: { ref: "myregistry.com/user/repo:cache" } }],
cacheTo: [{ registry: { ref: "myregistry.com/user/repo:cache" } }],
secrets: {
mysecret: "value",
},
addHosts: ["metadata.google.internal:169.254.169.254"],
ssh: {
default: ["/home/runner/.ssh/id_ed25519"],
},
network: "host",
platforms: ["linux/amd64"],
registries: [{
address: "myregistry.com",
username: "username",
password: pulumi.secret("password"),
}],
});
// v4 Image
const v4 = new docker.Image("v4-image", {
imageName: "myregistry.com/user/repo:latest",
skipPush: false,
build: {
dockerfile: "./Dockerfile",
context: "../app",
target: "mytarget",
args: {
MY_BUILD_ARG: "foo",
},
cacheFrom: {
images: ["myregistry.com/user/repo:cache"],
},
addHosts: ["metadata.google.internal:169.254.169.254"],
network: "host",
platform: "linux/amd64",
},
buildOnPreview: true,
registry: {
server: "myregistry.com",
username: "username",
password: pulumi.secret("password"),
},
});
// v4 Image after migrating to docker-build.Image
const v4Migrated = new dockerbuild.Image("v4-to-buildx", {
tags: ["myregistry.com/user/repo:latest"],
push: true,
dockerfile: {
location: "./Dockerfile",
},
context: {
location: "../app",
},
target: "mytarget",
buildArgs: {
MY_BUILD_ARG: "foo",
},
cacheFrom: [{ registry: { ref: "myregistry.com/user/repo:cache" } }],
cacheTo: [{ registry: { ref: "myregistry.com/user/repo:cache" } }],
addHosts: ["metadata.google.internal:169.254.169.254"],
network: "host",
platforms: ["linux/amd64"],
registries: [{
address: "myregistry.com",
username: "username",
password: pulumi.secret("password"),
}],
});
```
{{% /example %}}

View File

@@ -0,0 +1,408 @@
{{% examples %}}
## Example Usage
{{% example %}}
### Multi-platform registry caching
```typescript
import * as pulumi from "@pulumi/pulumi";
import * as docker_build from "@pulumi/docker-build";
const amd64 = new docker_build.Image("amd64", {
cacheFrom: [{
registry: {
ref: "docker.io/pulumi/pulumi:cache-amd64",
},
}],
cacheTo: [{
registry: {
mode: docker_build.CacheMode.Max,
ref: "docker.io/pulumi/pulumi:cache-amd64",
},
}],
context: {
location: "app",
},
platforms: [docker_build.Platform.Linux_amd64],
tags: ["docker.io/pulumi/pulumi:3.107.0-amd64"],
});
const arm64 = new docker_build.Image("arm64", {
cacheFrom: [{
registry: {
ref: "docker.io/pulumi/pulumi:cache-arm64",
},
}],
cacheTo: [{
registry: {
mode: docker_build.CacheMode.Max,
ref: "docker.io/pulumi/pulumi:cache-arm64",
},
}],
context: {
location: "app",
},
platforms: [docker_build.Platform.Linux_arm64],
tags: ["docker.io/pulumi/pulumi:3.107.0-arm64"],
});
const index = new docker_build.Index("index", {
sources: [
amd64.ref,
arm64.ref,
],
tag: "docker.io/pulumi/pulumi:3.107.0",
});
export const ref = index.ref;
```
```python
import pulumi
import pulumi_docker_build as docker_build
amd64 = docker_build.Image("amd64",
cache_from=[docker_build.CacheFromArgs(
registry=docker_build.CacheFromRegistryArgs(
ref="docker.io/pulumi/pulumi:cache-amd64",
),
)],
cache_to=[docker_build.CacheToArgs(
registry=docker_build.CacheToRegistryArgs(
mode=docker_build.CacheMode.MAX,
ref="docker.io/pulumi/pulumi:cache-amd64",
),
)],
context=docker_build.BuildContextArgs(
location="app",
),
platforms=[docker_build.Platform.LINUX_AMD64],
tags=["docker.io/pulumi/pulumi:3.107.0-amd64"])
arm64 = docker_build.Image("arm64",
cache_from=[docker_build.CacheFromArgs(
registry=docker_build.CacheFromRegistryArgs(
ref="docker.io/pulumi/pulumi:cache-arm64",
),
)],
cache_to=[docker_build.CacheToArgs(
registry=docker_build.CacheToRegistryArgs(
mode=docker_build.CacheMode.MAX,
ref="docker.io/pulumi/pulumi:cache-arm64",
),
)],
context=docker_build.BuildContextArgs(
location="app",
),
platforms=[docker_build.Platform.LINUX_ARM64],
tags=["docker.io/pulumi/pulumi:3.107.0-arm64"])
index = docker_build.Index("index",
sources=[
amd64.ref,
arm64.ref,
],
tag="docker.io/pulumi/pulumi:3.107.0")
pulumi.export("ref", index.ref)
```
```csharp
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using DockerBuild = Pulumi.DockerBuild;
return await Deployment.RunAsync(() =>
{
var amd64 = new DockerBuild.Image("amd64", new()
{
CacheFrom = new[]
{
new DockerBuild.Inputs.CacheFromArgs
{
Registry = new DockerBuild.Inputs.CacheFromRegistryArgs
{
Ref = "docker.io/pulumi/pulumi:cache-amd64",
},
},
},
CacheTo = new[]
{
new DockerBuild.Inputs.CacheToArgs
{
Registry = new DockerBuild.Inputs.CacheToRegistryArgs
{
Mode = DockerBuild.CacheMode.Max,
Ref = "docker.io/pulumi/pulumi:cache-amd64",
},
},
},
Context = new DockerBuild.Inputs.BuildContextArgs
{
Location = "app",
},
Platforms = new[]
{
DockerBuild.Platform.Linux_amd64,
},
Tags = new[]
{
"docker.io/pulumi/pulumi:3.107.0-amd64",
},
});
var arm64 = new DockerBuild.Image("arm64", new()
{
CacheFrom = new[]
{
new DockerBuild.Inputs.CacheFromArgs
{
Registry = new DockerBuild.Inputs.CacheFromRegistryArgs
{
Ref = "docker.io/pulumi/pulumi:cache-arm64",
},
},
},
CacheTo = new[]
{
new DockerBuild.Inputs.CacheToArgs
{
Registry = new DockerBuild.Inputs.CacheToRegistryArgs
{
Mode = DockerBuild.CacheMode.Max,
Ref = "docker.io/pulumi/pulumi:cache-arm64",
},
},
},
Context = new DockerBuild.Inputs.BuildContextArgs
{
Location = "app",
},
Platforms = new[]
{
DockerBuild.Platform.Linux_arm64,
},
Tags = new[]
{
"docker.io/pulumi/pulumi:3.107.0-arm64",
},
});
var index = new DockerBuild.Index("index", new()
{
Sources = new[]
{
amd64.Ref,
arm64.Ref,
},
Tag = "docker.io/pulumi/pulumi:3.107.0",
});
return new Dictionary<string, object?>
{
["ref"] = index.Ref,
};
});
```
```go
package main
import (
"github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
amd64, err := dockerbuild.NewImage(ctx, "amd64", &dockerbuild.ImageArgs{
CacheFrom: dockerbuild.CacheFromArray{
&dockerbuild.CacheFromArgs{
Registry: &dockerbuild.CacheFromRegistryArgs{
Ref: pulumi.String("docker.io/pulumi/pulumi:cache-amd64"),
},
},
},
CacheTo: dockerbuild.CacheToArray{
&dockerbuild.CacheToArgs{
Registry: &dockerbuild.CacheToRegistryArgs{
Mode: dockerbuild.CacheModeMax,
Ref: pulumi.String("docker.io/pulumi/pulumi:cache-amd64"),
},
},
},
Context: &dockerbuild.BuildContextArgs{
Location: pulumi.String("app"),
},
Platforms: docker - build.PlatformArray{
dockerbuild.Platform_Linux_amd64,
},
Tags: pulumi.StringArray{
pulumi.String("docker.io/pulumi/pulumi:3.107.0-amd64"),
},
})
if err != nil {
return err
}
arm64, err := dockerbuild.NewImage(ctx, "arm64", &dockerbuild.ImageArgs{
CacheFrom: dockerbuild.CacheFromArray{
&dockerbuild.CacheFromArgs{
Registry: &dockerbuild.CacheFromRegistryArgs{
Ref: pulumi.String("docker.io/pulumi/pulumi:cache-arm64"),
},
},
},
CacheTo: dockerbuild.CacheToArray{
&dockerbuild.CacheToArgs{
Registry: &dockerbuild.CacheToRegistryArgs{
Mode: dockerbuild.CacheModeMax,
Ref: pulumi.String("docker.io/pulumi/pulumi:cache-arm64"),
},
},
},
Context: &dockerbuild.BuildContextArgs{
Location: pulumi.String("app"),
},
Platforms: docker - build.PlatformArray{
dockerbuild.Platform_Linux_arm64,
},
Tags: pulumi.StringArray{
pulumi.String("docker.io/pulumi/pulumi:3.107.0-arm64"),
},
})
if err != nil {
return err
}
index, err := dockerbuild.NewIndex(ctx, "index", &dockerbuild.IndexArgs{
Sources: pulumi.StringArray{
amd64.Ref,
arm64.Ref,
},
Tag: pulumi.String("docker.io/pulumi/pulumi:3.107.0"),
})
if err != nil {
return err
}
ctx.Export("ref", index.Ref)
return nil
})
}
```
```yaml
description: Multi-platform registry caching
name: registry-caching
outputs:
ref: ${index.ref}
resources:
amd64:
properties:
cacheFrom:
- registry:
ref: docker.io/pulumi/pulumi:cache-amd64
cacheTo:
- registry:
mode: max
ref: docker.io/pulumi/pulumi:cache-amd64
context:
location: app
platforms:
- linux/amd64
tags:
- docker.io/pulumi/pulumi:3.107.0-amd64
type: docker-build:Image
arm64:
properties:
cacheFrom:
- registry:
ref: docker.io/pulumi/pulumi:cache-arm64
cacheTo:
- registry:
mode: max
ref: docker.io/pulumi/pulumi:cache-arm64
context:
location: app
platforms:
- linux/arm64
tags:
- docker.io/pulumi/pulumi:3.107.0-arm64
type: docker-build:Image
index:
properties:
sources:
- ${amd64.ref}
- ${arm64.ref}
tag: docker.io/pulumi/pulumi:3.107.0
type: docker-build:Index
runtime: yaml
```
```java
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.dockerbuild.Image;
import com.pulumi.dockerbuild.ImageArgs;
import com.pulumi.dockerbuild.inputs.CacheFromArgs;
import com.pulumi.dockerbuild.inputs.CacheFromRegistryArgs;
import com.pulumi.dockerbuild.inputs.CacheToArgs;
import com.pulumi.dockerbuild.inputs.CacheToRegistryArgs;
import com.pulumi.dockerbuild.inputs.BuildContextArgs;
import com.pulumi.dockerbuild.Index;
import com.pulumi.dockerbuild.IndexArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var amd64 = new Image("amd64", ImageArgs.builder()
.cacheFrom(CacheFromArgs.builder()
.registry(CacheFromRegistryArgs.builder()
.ref("docker.io/pulumi/pulumi:cache-amd64")
.build())
.build())
.cacheTo(CacheToArgs.builder()
.registry(CacheToRegistryArgs.builder()
.mode("max")
.ref("docker.io/pulumi/pulumi:cache-amd64")
.build())
.build())
.context(BuildContextArgs.builder()
.location("app")
.build())
.platforms("linux/amd64")
.tags("docker.io/pulumi/pulumi:3.107.0-amd64")
.build());
var arm64 = new Image("arm64", ImageArgs.builder()
.cacheFrom(CacheFromArgs.builder()
.registry(CacheFromRegistryArgs.builder()
.ref("docker.io/pulumi/pulumi:cache-arm64")
.build())
.build())
.cacheTo(CacheToArgs.builder()
.registry(CacheToRegistryArgs.builder()
.mode("max")
.ref("docker.io/pulumi/pulumi:cache-arm64")
.build())
.build())
.context(BuildContextArgs.builder()
.location("app")
.build())
.platforms("linux/arm64")
.tags("docker.io/pulumi/pulumi:3.107.0-arm64")
.build());
var index = new Index("index", IndexArgs.builder()
.sources(
amd64.ref(),
arm64.ref())
.tag("docker.io/pulumi/pulumi:3.107.0")
.build());
ctx.export("ref", index.ref());
}
}
```
{{% /example %}}
{{% /examples %}}

475
provider/internal/export.go Normal file
View File

@@ -0,0 +1,475 @@
// 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 (
"errors"
"fmt"
"slices"
"strings"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/util/buildflags"
"github.com/pulumi/pulumi-go-provider/infer"
)
var (
_ fmt.Stringer = (*Export)(nil)
_ fmt.Stringer = (*ExportDocker)(nil)
_ fmt.Stringer = (*ExportImage)(nil)
_ fmt.Stringer = (*ExportLocal)(nil)
_ fmt.Stringer = (*ExportOCI)(nil)
_ fmt.Stringer = (*ExportRegistry)(nil)
_ fmt.Stringer = (*ExportTar)(nil)
_ fmt.Stringer = ExportWithAnnotations{}
_ fmt.Stringer = ExportWithCompression{}
_ fmt.Stringer = ExportWithNames{}
_ fmt.Stringer = ExportWithOCI{}
_ infer.Annotated = (*Export)(nil)
_ infer.Annotated = (*ExportDocker)(nil)
_ infer.Annotated = (*ExportImage)(nil)
_ infer.Annotated = (*ExportLocal)(nil)
_ infer.Annotated = (*ExportOCI)(nil)
_ infer.Annotated = (*ExportRegistry)(nil)
_ infer.Annotated = (*ExportTar)(nil)
)
// Export is a "union" type for all of our available `--output` options.
type Export struct {
Tar *ExportTar `pulumi:"tar,optional"`
Local *ExportLocal `pulumi:"local,optional"`
Registry *ExportRegistry `pulumi:"registry,optional"`
Image *ExportImage `pulumi:"image,optional"`
OCI *ExportOCI `pulumi:"oci,optional"`
Docker *ExportDocker `pulumi:"docker,optional"`
CacheOnly *ExportCacheOnly `pulumi:"cacheonly,optional"`
Raw Raw `pulumi:"raw,optional"`
Disabled bool `pulumi:"disabled,optional"`
}
// Annotate sets docstrings on Export.
func (e *Export) Annotate(a infer.Annotator) {
a.Describe(&e.Tar, dedent(`
Export to a local directory as a tarball.`,
))
a.Describe(&e.Local, dedent(`
Export to a local directory as files and directories.`,
))
a.Describe(&e.Registry, dedent(`
Identical to the Image exporter, but pushes by default.`,
))
a.Describe(&e.Image, dedent(`
Outputs the build result into a container image format.`,
))
a.Describe(&e.OCI, dedent(`
Identical to the Docker exporter but uses OCI media types by default.`,
))
a.Describe(&e.Docker, dedent(`
Export as a Docker image layout.`,
))
a.Describe(&e.Raw, dedent(`
A raw string as you would provide it to the Docker CLI (e.g.,
"type=docker")`,
))
a.Describe(&e.CacheOnly, dedent(`
A no-op export. Helpful for silencing the 'no exports' warning if you
just want to populate caches.
`))
a.Describe(&e.Disabled, dedent(`
When "true" this entry will be excluded. Defaults to "false".
`))
}
// String returns a CLI-encoded value for this `--output` entry, or an empty
// string if disabled. `validate` should be called to ensure only one entry was
// set.
func (e Export) String() string {
if e.Disabled {
return ""
}
return join(e.Tar, e.Local, e.Registry, e.Image, e.OCI, e.Docker, e.CacheOnly, e.Raw)
}
// pushed returns true if the export would result in a registry push.
func (e Export) pushed() bool {
if e.Raw != "" {
exp, err := buildflags.ParseExports([]string{e.Raw.String()})
if err != nil {
return false
}
return exp[0].Attrs["push"] == "true"
}
if e.Registry != nil {
return e.Registry.Push == nil || *e.Registry.Push
}
if e.Image != nil {
return e.Image.Push != nil && *e.Image.Push
}
return false
}
func (e Export) validate(preview bool, tags []string) (*controllerapi.ExportEntry, error) {
if strings.Count(e.String(), "type=") > 1 {
return nil, errors.New("exports should only specify one export type")
}
ee, err := buildflags.ParseExports([]string{e.String()})
if err != nil {
return nil, err
}
exp := ee[0]
if len(tags) == 0 && isRegistryPush(exp) && exp.Attrs["name"] == "" {
return nil, errors.New(
"at least one tag or export name is needed when pushing to a registry",
)
}
if !preview {
return exp, nil
}
// Don't perform registry pushes during previews.
if exp.Type == "image" {
exp.Attrs["push"] = "false"
}
return exp, nil
}
// ExportCacheOnly is a dummy/no-op --cache-to entry. It exists only to help
// silence the "no exports configured" warning. By using this the user signals
// that they intentionally do not want exports, and only caches will be
// populated as a result.
type ExportCacheOnly struct{}
// String returns the CLI-encoded value of these export options, or an empty
// string if the receiver is nil.
func (e *ExportCacheOnly) String() string {
if e == nil {
return ""
}
return "type=cacheonly"
}
// ExportDocker pushes the final image to the local build daemon.
type ExportDocker struct {
ExportWithOCI
ExportWithCompression
ExportWithAnnotations
ExportWithNames
Dest string `pulumi:"dest,optional"`
Tar *bool `pulumi:"tar,optional"`
}
// Annotate sets docstrings and defaults on ExportDocker.
func (e *ExportDocker) Annotate(a infer.Annotator) {
a.SetDefault(&e.Tar, true)
a.Describe(&e.Dest, "The local export path.")
a.Describe(&e.Tar, "Bundle the output into a tarball layout.")
}
// String returns the CLI-encoded value of these export options, or an empty
// string if the receiver is nil.
func (e *ExportDocker) String() string {
if e == nil {
return ""
}
parts := []string{}
if e.Dest != "" {
parts = append(parts, "dest="+e.Dest)
}
if e.Tar != nil {
parts = append(parts, fmt.Sprintf("tar=%t", *e.Tar))
}
return join(
Raw("type=docker"),
Raw(strings.Join(parts, ",")),
e.ExportWithOCI,
e.ExportWithCompression,
e.ExportWithAnnotations,
e.ExportWithNames,
)
}
// ExportOCI is a cache that defaults to using OCI media types.
type ExportOCI struct {
ExportDocker
}
// Annotate sets docstrings and defaults on ExportOCI.
func (e *ExportOCI) Annotate(a infer.Annotator) {
a.SetDefault(&e.OCI, true)
a.Describe(&e.OCI, "Use OCI media types in exporter manifests.")
}
func (e *ExportOCI) String() string {
if e == nil {
return ""
}
return strings.Replace(e.ExportDocker.String(), "type=docker", "type=oci", 1)
}
// ExportImage can push the final image to remote registries.
type ExportImage struct {
ExportWithOCI
ExportWithCompression
ExportWithNames
ExportWithAnnotations
Push *bool `pulumi:"push,optional"`
PushByDigest *bool `pulumi:"pushByDigest,optional"`
Insecure *bool `pulumi:"insecure,optional"`
DanglingNamePrefix string `pulumi:"danglingNamePrefix,optional"`
NameCanonical *bool `pulumi:"nameCanonical,optional"`
Unpack *bool `pulumi:"unpack,optional"`
Store *bool `pulumi:"store,optional"`
}
// Annotate sets docstrings and defaults on ExportImage.
func (e *ExportImage) Annotate(a infer.Annotator) {
a.SetDefault(&e.Store, true)
a.Describe(&e.Store, dedent(`
Store resulting images to the worker's image store and ensure all of
its blobs are in the content store.
Defaults to "true".
Ignored if the worker doesn't have image store (when using OCI workers,
for example).
`))
a.Describe(&e.Push, dedent(`
Push after creating the image. Defaults to "false".
`))
a.Describe(&e.DanglingNamePrefix, dedent(`
Name image with "prefix@<digest>", used for anonymous images.
`))
a.Describe(&e.NameCanonical, dedent(`
Add additional canonical name ("name@<digest>").
`))
a.Describe(&e.Insecure, dedent(`
Allow pushing to an insecure registry.
`))
a.Describe(&e.PushByDigest, dedent(`
Push image without name.
`))
a.Describe(&e.Unpack, dedent(`
Unpack image after creation (for use with containerd). Defaults to
"false".
`))
}
// String returns the CLI-encoded value of these export options, or an empty
// string if the receiver is nil.
func (e *ExportImage) String() string {
if e == nil {
return ""
}
parts := []string{}
if e.Push != nil {
parts = append(parts, fmt.Sprintf("push=%t", *e.Push))
}
if e.PushByDigest != nil {
parts = append(parts, fmt.Sprintf("push-by-digest=%t", *e.PushByDigest))
}
if e.Insecure != nil {
parts = append(parts, fmt.Sprintf("insecure=%t", *e.Insecure))
}
if e.DanglingNamePrefix != "" {
parts = append(parts, "dangling-name-prefix="+e.DanglingNamePrefix)
}
if e.NameCanonical != nil {
parts = append(parts, fmt.Sprintf("name-canonical=%t", *e.NameCanonical))
}
if e.Unpack != nil {
parts = append(parts, fmt.Sprintf("unpack=%t", *e.Unpack))
}
if e.Store != nil {
parts = append(parts, fmt.Sprintf("store=%t", *e.Store))
}
return join(
Raw("type=image"),
Raw(strings.Join(parts, ",")),
e.ExportWithOCI,
e.ExportWithCompression,
e.ExportWithNames,
e.ExportWithAnnotations,
)
}
// ExportRegistry is equivalent to ExportImage but defaults to push=true.
type ExportRegistry struct {
ExportImage
}
// Annotate sets docstrings and defaults on ExportRegistry.
func (e *ExportRegistry) Annotate(a infer.Annotator) {
a.Describe(&e.Push, dedent(`
Push after creating the image. Defaults to "true".
`))
a.SetDefault(&e.Push, true)
}
// String returns the CLI-encoded value of these export options, or an empty
// string if the receiver is nil.
func (e *ExportRegistry) String() string {
if e == nil {
return ""
}
return strings.Replace(e.ExportImage.String(), "type=image", "type=registry", 1)
}
// ExportLocal writes the final image to disk.
type ExportLocal struct {
Dest string `pulumi:"dest"`
}
// String returns the CLI-encoded value of these export options, or an empty
// string if the receiver is nil.
func (e *ExportLocal) String() string {
if e == nil {
return ""
}
return "type=local,dest=" + e.Dest
}
// Annotate sets docstrings on ExportLocal.
func (e *ExportLocal) Annotate(a infer.Annotator) {
a.Describe(&e.Dest, "Output path.")
}
// ExportTar is an export that uses the tar format for exporting.
type ExportTar struct {
ExportLocal
}
func (e *ExportTar) String() string {
if e == nil {
return ""
}
return "type=tar,dest=" + e.Dest
}
// ExportWithOCI is an export that support OCI media types.
type ExportWithOCI struct {
OCI *bool `pulumi:"ociMediaTypes,optional"`
}
// Annotate sets defaults on ExportWithOCI.
func (c *ExportWithOCI) Annotate(a infer.Annotator) {
a.SetDefault(&c.OCI, false)
a.Describe(&c.OCI, "Use OCI media types in exporter manifests.")
}
func (c ExportWithOCI) String() string {
if c.OCI == nil {
return ""
}
return fmt.Sprintf("oci-mediatypes=%t", *c.OCI)
}
// ExportWithCompression is an export with options to configure compression
// settings.
type ExportWithCompression struct {
Compression *CompressionType `pulumi:"compression,optional"`
CompressionLevel int `pulumi:"compressionLevel,optional"`
ForceCompression *bool `pulumi:"forceCompression,optional"`
}
// Annotate sets docstrings and defaults on ExportWithCompression.
func (e *ExportWithCompression) Annotate(a infer.Annotator) {
gzip := Gzip
a.SetDefault(&e.Compression, &gzip)
a.SetDefault(&e.CompressionLevel, 0)
a.SetDefault(&e.ForceCompression, false)
a.Describe(&e.Compression, "The compression type to use.")
a.Describe(&e.CompressionLevel, "Compression level from 0 to 22.")
a.Describe(&e.ForceCompression, "Forcefully apply compression.")
}
func (e ExportWithCompression) String() string {
if e.CompressionLevel == 0 {
return ""
}
parts := []string{}
if e.Compression != nil {
parts = append(parts, fmt.Sprintf("compression=%s", *e.Compression))
}
if e.CompressionLevel > 0 {
cl := e.CompressionLevel
if cl > 22 {
cl = 22
}
parts = append(parts, fmt.Sprintf("compression-level=%d", cl))
}
if e.ForceCompression != nil {
parts = append(parts, fmt.Sprintf("force-compression=%t", *e.ForceCompression))
}
return strings.Join(parts, ",")
}
// ExportWithNames is an export with configurable names (tags).
type ExportWithNames struct {
Names []string `pulumi:"names,optional"`
}
func (e ExportWithNames) String() string {
parts := []string{}
for _, n := range e.Names {
parts = append(parts, "name="+n)
}
return strings.Join(parts, ",")
}
// Annotate sets docstrings on ExportWithNames.
func (e *ExportWithNames) Annotate(a infer.Annotator) {
a.Describe(
&e.Names,
"Specify images names to export. This is overridden if tags are already specified.",
)
}
// ExportWithAnnotations is an export with configurable annotations.
type ExportWithAnnotations struct {
Annotations map[string]string `pulumi:"annotations,optional"`
}
func (e ExportWithAnnotations) String() string {
parts := []string{}
for k, v := range e.Annotations {
parts = append(parts, fmt.Sprintf("annotation.%s=%s", k, v))
}
slices.Sort(parts)
return strings.Join(parts, ",")
}
// Annotate sets docstrings on ExportWithAnnotations.
func (e *ExportWithAnnotations) Annotate(a infer.Annotator) {
a.Describe(&e.Annotations, dedent(`
Attach an arbitrary key/value annotation to the image.
`))
}
// isRegistryPush returns true if the ExportEntry results in an image pushed to
// a registry.
func isRegistryPush(export *controllerapi.ExportEntry) bool {
// "type=registry" is shorthand for "type=image,push=true" so we only need
// to check "image" types.
return export.Type == "image" && export.Attrs["push"] == "true"
}

View File

@@ -0,0 +1,246 @@
// 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 (
"testing"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/util/buildflags"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func TestValidateExport(t *testing.T) {
t.Parallel()
tests := []struct {
name string
e Export
givenTags []string
preview bool
wantExp *controllerapi.ExportEntry
wantErr string
}{
{
name: "raw - no push on preview",
preview: true,
e: Export{Raw: "type=registry"},
givenTags: []string{"docker.io/foo/bar"},
wantExp: &controllerapi.ExportEntry{
Type: "image",
Attrs: map[string]string{"push": "false"},
},
},
{
name: "raw - push requires tags",
e: Export{Raw: "type=registry"},
wantErr: "tag or export name is needed",
},
{
name: "registry - no push on preview",
preview: true,
e: Export{Registry: &ExportRegistry{}},
givenTags: []string{"docker.io/foo/bar"},
wantExp: &controllerapi.ExportEntry{
Type: "image",
Attrs: map[string]string{"push": "false"},
},
},
{
name: "registry - push requires tags",
e: Export{Registry: &ExportRegistry{}},
wantErr: "tag or export name is needed",
},
{
name: "over-specified",
e: Export{Raw: "type=registry", Registry: &ExportRegistry{}},
wantErr: "specify one export type",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
e, err := tt.e.validate(tt.preview, tt.givenTags)
if tt.wantErr == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.wantErr)
}
if tt.wantExp != nil {
assert.Equal(t, tt.wantExp.Type, e.Type)
assert.Equal(t, tt.wantExp.Attrs, e.Attrs)
}
})
}
}
func TestExportString(t *testing.T) {
t.Parallel()
gzip := Gzip
tests := []struct {
name string
given Export
want string
}{
{
name: "tar",
given: Export{Tar: &ExportTar{ExportLocal: ExportLocal{Dest: "/foo"}}},
want: "type=tar,dest=/foo",
},
{
name: "local",
given: Export{Local: &ExportLocal{Dest: "/bar"}},
want: "type=local,dest=/bar",
},
{
name: "registry-with-compression",
given: Export{Registry: &ExportRegistry{
ExportImage: ExportImage{
ExportWithCompression: ExportWithCompression{
Compression: &gzip,
CompressionLevel: 100,
ForceCompression: pulumi.BoolRef(true),
},
},
}},
want: "type=registry,compression=gzip,compression-level=22,force-compression=true",
},
{
name: "registry-without-push",
given: Export{Registry: &ExportRegistry{
ExportImage: ExportImage{
Push: pulumi.BoolRef(false),
},
}},
want: "type=registry,push=false",
},
{
name: "image",
given: Export{
Image: &ExportImage{
Push: pulumi.BoolRef(true),
PushByDigest: pulumi.BoolRef(true),
Insecure: pulumi.BoolRef(true),
DanglingNamePrefix: "prefix",
Unpack: pulumi.BoolRef(true),
Store: pulumi.BoolRef(false),
},
},
want: "type=image,push=true,push-by-digest=true,insecure=true,dangling-name-prefix=prefix,unpack=true,store=false",
},
{
name: "oci-with-names",
given: Export{OCI: &ExportOCI{
ExportDocker: ExportDocker{
ExportWithNames: ExportWithNames{
Names: []string{"foo", "bar"},
},
},
}},
want: "type=oci,name=foo,name=bar",
},
{
name: "docker-with-annotations",
given: Export{Docker: &ExportDocker{
ExportWithAnnotations: ExportWithAnnotations{
Annotations: map[string]string{
"foo": "bar",
"boo": "baz",
},
},
}},
want: "type=docker,annotation.boo=baz,annotation.foo=bar",
},
{
name: "raw",
given: Export{Raw: Raw("type=docker")},
want: "type=docker",
},
{
name: "disabled",
given: Export{Raw: Raw("type=docker"), Disabled: true},
want: "",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := tt.given.String()
assert.Equal(t, tt.want, tt.given.String())
if tt.want != "" {
// Our output should be parsable by Docker.
_, err := buildflags.ParseExports([]string{actual})
assert.NoError(t, err)
}
})
}
}
func TestExportPushed(t *testing.T) {
t.Parallel()
tests := []struct {
name string
e Export
want bool
}{
{
name: "raw registry",
e: Export{Raw: "type=registry"},
want: true,
},
{
name: "raw image",
e: Export{Raw: "type=image"},
want: false,
},
{
name: "registry with no push",
e: Export{Registry: &ExportRegistry{}},
want: true,
},
{
name: "registry with explicit push",
e: Export{Registry: &ExportRegistry{ExportImage{Push: pulumi.BoolRef(false)}}},
want: false,
},
{
name: "image with explicit push",
e: Export{Image: &ExportImage{Push: pulumi.BoolRef(true)}},
want: true,
},
{
name: "local",
e: Export{Local: &ExportLocal{}},
want: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := tt.e.pushed()
assert.Equal(t, tt.want, actual)
})
}
}

163
provider/internal/host.go Normal file
View File

@@ -0,0 +1,163 @@
// 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 (
"context"
"path/filepath"
"sync"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/store/storeutil"
"github.com/docker/cli/cli/command"
cfgtypes "github.com/docker/cli/cli/config/types"
)
// host contains a host-level Docker CLI as well as a cache of initialized
// builders. Operations on the host are serialized.
type host struct {
mu sync.Mutex
cli command.Cli
config *Config
builders map[string]*cachedBuilder
auths map[string]cfgtypes.AuthConfig
}
func newHost(config *Config) (*host, error) {
docker, err := newDockerCLI(config)
if err != nil {
return nil, err
}
// Load existing credentials into memory.
auths, err := docker.ConfigFile().GetAllCredentials()
if err != nil {
return nil, err
}
h := &host{
cli: docker,
config: config,
builders: map[string]*cachedBuilder{},
auths: auths,
}
return h, err
}
// builderFor ensures a builder is available and running. This is guarded by a
// mutex to ensure other resources don't attempt to use the builder until it's
// ready.
//
// If the build doesn't specify a builder by name, we will iterate through all
// available builders until we find one that we can connect to.
func (h *host) builderFor(build Build) (*cachedBuilder, error) {
h.mu.Lock()
defer h.mu.Unlock()
opts := build.BuildOptions()
if b, ok := h.builders[opts.Builder]; ok {
return b, nil
}
txn, release, err := storeutil.GetStore(h.cli)
if err != nil {
return nil, err
}
defer release()
contextPathHash := opts.ContextPath
if absContextPath, err := filepath.Abs(contextPathHash); err == nil {
contextPathHash = absContextPath
}
b, err := builder.New(h.cli,
builder.WithName(opts.Builder),
builder.WithContextPathHash(contextPathHash),
builder.WithStore(txn),
)
if err != nil {
return nil, err
}
// If we didn't request a particular builder, and we loaded a default
// builder with an unsupported (docker) driver, then look for a builder we
// do support.
if b.Driver == "" && opts.Builder == "" {
builders, err := builder.GetBuilders(h.cli, txn)
if err != nil {
return nil, err
}
nextbuilder:
for _, bb := range builders {
if bb.Driver == "" {
continue
}
if err := bb.Validate(); err != nil {
continue
}
if bb.Err() != nil {
continue
}
nodes, err := bb.LoadNodes(context.Background())
if err != nil {
continue
}
for idx := range nodes {
n := nodes[idx]
if n.Driver == nil {
continue nextbuilder
}
if _, err := n.Driver.Dial(context.Background()); err != nil {
continue nextbuilder
}
// TODO: Confirm the builder supports the requested platforms.
}
b = bb
break
}
}
if b.Driver == "" && opts.Builder == "" {
// If we STILL don't have a builder, create a docker-container instance.
b, err = builder.Create(
context.Background(),
txn,
h.cli,
builder.CreateOpts{Driver: "docker-container"},
)
if err != nil {
return nil, err
}
}
// Attempt to load nodes in order to determine the builder's driver. Ignore
// errors for "exec" builds because it's possible to request builders with
// drivers that are unknown to us.
nodes, err := b.LoadNodes(context.Background())
if err != nil && !build.ShouldExec() {
return nil, err
}
cached := &cachedBuilder{name: b.Name, driver: b.Driver, nodes: nodes}
h.builders[opts.Builder] = cached
return cached, nil
}
// cachedBuilder caches the builders we've loaded. Repeatedly fetching them can
// sometimes result in EOF errors from the daemon, especially when under load.
type cachedBuilder struct {
name string
driver string
nodes []builder.Node
}

1012
provider/internal/image.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,994 @@
// 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 (
"errors"
"fmt"
"os"
"path/filepath"
"testing"
_ "github.com/docker/buildx/driver/docker-container"
"github.com/docker/distribution/reference"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/regclient/regclient/types/descriptor"
"github.com/regclient/regclient/types/platform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
provider "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi-go-provider/integration"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/mapper"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
var _fakeURN = resource.NewURN("test", "provider", "a", "docker-build:index:Image", "test")
func TestImageLifecycle(t *testing.T) {
t.Parallel()
noClient := func(t *testing.T) Client {
ctrl := gomock.NewController(t)
return NewMockClient(ctrl)
}
_, err := reference.ParseNamed("docker.io/pulumibot/buildkit-e2e")
require.NoError(t, err)
tests := []struct {
name string
op func(t *testing.T) integration.Operation
client func(t *testing.T) Client
}{
{
name: "happy path builds",
client: func(t *testing.T) Client {
ctrl := gomock.NewController(t)
c := NewMockClient(ctrl)
c.EXPECT().BuildKitEnabled().Return(true, nil).AnyTimes()
c.EXPECT().Build(gomock.Any(), gomock.AssignableToTypeOf(build{})).DoAndReturn(
func(_ provider.Context, b Build) (*client.SolveResponse, error) {
assert.Equal(t, "testdata/noop/Dockerfile", b.BuildOptions().DockerfileName)
return &client.SolveResponse{
ExporterResponse: map[string]string{
exptypes.ExporterImageDigestKey: "sha256:98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4",
},
}, nil
},
).AnyTimes()
c.EXPECT().Delete(gomock.Any(),
"docker.io/pulumibot/buildkit-e2e@sha256:98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4",
).
Return(nil)
return c
},
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"push": resource.NewBoolProperty(false),
"tags": resource.NewArrayProperty(
[]resource.PropertyValue{
resource.NewStringProperty("docker.io/pulumibot/buildkit-e2e"),
resource.NewStringProperty("docker.io/pulumibot/buildkit-e2e:main"),
},
),
"platforms": resource.NewArrayProperty(
[]resource.PropertyValue{
resource.NewStringProperty("linux/arm64"),
resource.NewStringProperty("linux/amd64"),
},
),
"context": resource.NewObjectProperty(resource.PropertyMap{
"location": resource.NewStringProperty("testdata/noop"),
}),
"dockerfile": resource.NewObjectProperty(resource.PropertyMap{
"location": resource.NewStringProperty("testdata/noop/Dockerfile"),
}),
"exports": resource.NewArrayProperty(
[]resource.PropertyValue{
resource.NewObjectProperty(resource.PropertyMap{
"raw": resource.NewStringProperty("type=registry"),
},
),
},
),
"registries": resource.NewArrayProperty(
[]resource.PropertyValue{
resource.NewObjectProperty(resource.PropertyMap{
"address": resource.NewStringProperty("fakeaddress"),
"username": resource.NewStringProperty("fakeuser"),
"password": resource.MakeSecret(
resource.NewStringProperty("password"),
),
}),
},
),
},
}
},
},
{
name: "tags are required when pushing",
client: noClient,
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"push": resource.NewBoolProperty(false),
"tags": resource.NewArrayProperty([]resource.PropertyValue{}),
"context": resource.NewObjectProperty(resource.PropertyMap{
"location": resource.NewStringProperty("testdata/noop"),
}),
"exports": resource.NewArrayProperty(
[]resource.PropertyValue{
resource.NewObjectProperty(resource.PropertyMap{
"raw": resource.NewStringProperty("type=registry"),
}),
},
),
},
ExpectFailure: true,
CheckFailures: []provider.CheckFailure{
{
Property: "exports[0]",
Reason: "at least one tag or export name is needed when pushing to a registry",
},
},
}
},
},
{
name: "invalid exports",
client: noClient,
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"push": resource.NewBoolProperty(false),
"tags": resource.NewArrayProperty(
[]resource.PropertyValue{resource.NewStringProperty("invalid-exports")},
),
"exports": resource.NewArrayProperty(
[]resource.PropertyValue{
resource.NewObjectProperty(resource.PropertyMap{
"raw": resource.NewStringProperty("type="),
}),
},
),
},
ExpectFailure: true,
CheckFailures: []provider.CheckFailure{{
Property: "exports[0]",
Reason: "type is required for output",
}},
}
},
},
{
name: "requires buildkit",
client: func(t *testing.T) Client {
ctrl := gomock.NewController(t)
c := NewMockClient(ctrl)
gomock.InOrder(
c.EXPECT().BuildKitEnabled().Return(false, nil), // Preview.
)
return c
},
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"push": resource.NewBoolProperty(false),
"tags": resource.NewArrayProperty(
[]resource.PropertyValue{resource.NewStringProperty("foo")},
),
"context": resource.NewObjectProperty(resource.PropertyMap{
"location": resource.NewStringProperty("testdata/noop"),
}),
},
ExpectFailure: true,
}
},
},
{
name: "error reading DOCKER_BUILDKIT",
client: func(t *testing.T) Client {
ctrl := gomock.NewController(t)
c := NewMockClient(ctrl)
gomock.InOrder(
c.EXPECT().
BuildKitEnabled().
Return(false, errors.New("invalid DOCKER_BUILDKIT")), // Preview.
)
return c
},
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"push": resource.NewBoolProperty(false),
"tags": resource.NewArrayProperty(
[]resource.PropertyValue{resource.NewStringProperty("foo")},
),
"context": resource.NewObjectProperty(resource.PropertyMap{
"location": resource.NewStringProperty("testdata/noop"),
}),
},
ExpectFailure: true,
}
},
},
{
name: "file defaults to Dockerfile",
client: func(t *testing.T) Client {
ctrl := gomock.NewController(t)
c := NewMockClient(ctrl)
c.EXPECT().BuildKitEnabled().Return(true, nil).AnyTimes()
c.EXPECT().Build(gomock.Any(), gomock.AssignableToTypeOf(build{})).DoAndReturn(
func(_ provider.Context, b Build) (*client.SolveResponse, error) {
assert.Equal(t, "testdata/noop/Dockerfile", b.BuildOptions().DockerfileName)
return &client.SolveResponse{
ExporterResponse: map[string]string{"image.name": "test:latest"},
}, nil
},
).AnyTimes()
c.EXPECT().Delete(gomock.Any(), "default-dockerfile").Return(nil)
return c
},
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"push": resource.NewBoolProperty(false),
"tags": resource.NewArrayProperty(
[]resource.PropertyValue{
resource.NewStringProperty("default-dockerfile"),
},
),
"context": resource.NewObjectProperty(resource.PropertyMap{
"location": resource.NewStringProperty("testdata/noop"),
}),
},
Hook: func(_, output resource.PropertyMap) {
dockerfile := output["dockerfile"]
require.NotNil(t, dockerfile)
require.True(t, dockerfile.IsObject())
location := dockerfile.ObjectValue()["location"]
require.True(t, location.IsString())
assert.Equal(t, "testdata/noop/Dockerfile", location.StringValue())
},
}
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
lc := integration.LifeCycleTest{
Resource: "docker-build:index:Image",
Create: tt.op(t),
}
s := newServer(tt.client(t))
err := s.Configure(provider.ConfigureRequest{})
require.NoError(t, err)
lc.Run(t, s)
})
}
}
type errNotFound struct{}
func (errNotFound) NotFound() {}
func (errNotFound) Error() string { return "not found " }
func TestDelete(t *testing.T) {
t.Parallel()
t.Run("image was already deleted", func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
client := NewMockClient(ctrl)
client.EXPECT().
Delete(gomock.Any(), "docker.io/pulumi/test@sha256:foo").
Return(errNotFound{})
s := newServer(client)
err := s.Configure(provider.ConfigureRequest{})
require.NoError(t, err)
err = s.Delete(provider.DeleteRequest{
ID: "foo,bar",
Urn: _fakeURN,
Properties: resource.PropertyMap{
"tags": resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty("docker.io/pulumi/test:foo"),
}),
"push": resource.NewBoolProperty(true),
"digest": resource.NewStringProperty("sha256:foo"),
"contextHash": resource.NewStringProperty(""),
"ref": resource.NewStringProperty(""),
},
})
assert.NoError(t, err)
})
}
func TestRead(t *testing.T) {
t.Parallel()
tag := "docker.io/pulumi/pulumitest"
digest := "sha256:3be99cafdcd80a8e620da56bdc215acab6213bb608d3d492c0ba1807128786a1"
ctrl := gomock.NewController(t)
client := NewMockClient(ctrl)
client.EXPECT().Inspect(gomock.Any(), fmt.Sprintf("%s:latest@%s", tag, digest)).Return(
[]descriptor.Descriptor{
{
Platform: &platform.Platform{Architecture: "arm64"},
},
{
Platform: &platform.Platform{Architecture: "unknown"},
},
}, nil)
s := newServer(client)
err := s.Configure(provider.ConfigureRequest{})
require.NoError(t, err)
resp, err := s.Read(provider.ReadRequest{
ID: "my-image",
Urn: _fakeURN,
Properties: resource.PropertyMap{
"exports": resource.NewArrayProperty([]resource.PropertyValue{
resource.NewObjectProperty(resource.PropertyMap{
"raw": resource.NewStringProperty("type=registry"),
}),
}),
"tags": resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty(tag),
}),
"digest": resource.NewStringProperty(digest),
},
})
require.NoError(t, err)
assert.NotNil(t, resp.Properties["exports"].ArrayValue()[0].ObjectValue()["manifest"])
}
func TestImageDiff(t *testing.T) {
t.Parallel()
emptyDir := t.TempDir()
host := Host
hash, err := hashBuildContext(emptyDir, "", nil)
require.NoError(t, err)
baseArgs := ImageArgs{
Context: &BuildContext{Context: Context{Location: emptyDir}},
Dockerfile: &Dockerfile{Location: "testdata/noop"},
Tags: []string{},
}
baseState := ImageState{
ContextHash: hash,
ImageArgs: baseArgs,
}
tests := []struct {
name string
olds func(*testing.T, ImageState) ImageState
news func(*testing.T, ImageArgs) ImageArgs
wantChanges bool
}{
{
name: "no diff if build context is unchanged",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(*testing.T, ImageArgs) ImageArgs { return baseArgs },
wantChanges: false,
},
{
name: "no diff if registry password changes",
olds: func(_ *testing.T, s ImageState) ImageState {
s.Registries = []Registry{{
Address: "foo",
Username: "foo",
Password: "foo",
}}
return s
},
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Registries = []Registry{{
Address: "foo",
Username: "foo",
Password: "DIFFERENT PASSWORD",
}}
return a
},
wantChanges: false,
},
{
name: "no diff if pull=true but no exports",
olds: func(_ *testing.T, is ImageState) ImageState {
is.Pull = true
return is
},
news: func(t *testing.T, ia ImageArgs) ImageArgs {
ia.Pull = true
return ia
},
wantChanges: false,
},
{
name: "diff if pull=true with exports",
olds: func(_ *testing.T, is ImageState) ImageState {
is.Pull = true
is.Load = true
return is
},
news: func(t *testing.T, ia ImageArgs) ImageArgs {
ia.Pull = true
ia.Load = true
return ia
},
wantChanges: true,
},
{
name: "diff if build context changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(t *testing.T, a ImageArgs) ImageArgs {
tmp := filepath.Join(a.Context.Location, "tmp")
err := os.WriteFile(tmp, []byte{}, 0o600)
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(tmp) })
return a
},
wantChanges: true,
},
{
name: "diff if registry added",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Registries = []Registry{{}}
return a
},
wantChanges: true,
},
{
name: "diff if registry user changes",
olds: func(_ *testing.T, s ImageState) ImageState {
s.Registries = []Registry{{
Address: "foo",
Username: "foo",
Password: "foo",
}}
return s
},
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Registries = []Registry{{
Address: "DIFFERENT USER",
Username: "foo",
Password: "foo",
}}
return a
},
wantChanges: true,
},
{
name: "diff if buildArgs changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.BuildArgs = map[string]string{
"foo": "bar",
}
return a
},
wantChanges: true,
},
{
name: "diff if pull changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(t *testing.T, ia ImageArgs) ImageArgs {
ia.Pull = true
return ia
},
wantChanges: true,
},
{
name: "diff if load changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(t *testing.T, ia ImageArgs) ImageArgs {
ia.Load = true
return ia
},
wantChanges: true,
},
{
name: "diff if push changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(t *testing.T, ia ImageArgs) ImageArgs {
ia.Push = true
return ia
},
wantChanges: true,
},
{
name: "diff if buildOnPreview doesn't change",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(t *testing.T, ia ImageArgs) ImageArgs {
val := true
ia.BuildOnPreview = &val
return ia
},
wantChanges: true,
},
{
name: "diff if buildOnPreview changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(t *testing.T, ia ImageArgs) ImageArgs {
val := false
ia.BuildOnPreview = &val
return ia
},
wantChanges: true,
},
{
name: "diff if ssh changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(t *testing.T, ia ImageArgs) ImageArgs {
ia.SSH = []SSH{{ID: "default"}}
return ia
},
wantChanges: true,
},
{
name: "diff if hosts change",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(t *testing.T, ia ImageArgs) ImageArgs {
ia.AddHosts = []string{"localhost"}
return ia
},
wantChanges: true,
},
{
name: "diff if cacheFrom changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.CacheFrom = []CacheFrom{{Raw: "a"}}
return a
},
wantChanges: true,
},
{
name: "diff if cacheTo changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.CacheTo = []CacheTo{{Raw: "a"}}
return a
},
wantChanges: true,
},
{
name: "diff if context changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Context = &BuildContext{Context: Context{Location: "testdata/ignores"}}
return a
},
wantChanges: true,
},
{
name: "diff if named context changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Context = &BuildContext{Named: NamedContexts{"foo": Context{Location: "bar"}}}
return a
},
wantChanges: true,
},
{
name: "diff if network changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Network = &host
return a
},
wantChanges: true,
},
{
name: "diff if dockerfile location changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Dockerfile = &Dockerfile{Location: "testdata/ignores/basedir/Dockerfile"}
return a
},
wantChanges: true,
},
{
name: "diff if dockerfile inline changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Dockerfile = &Dockerfile{Inline: "FROM scratch"}
return a
},
wantChanges: true,
},
{
name: "diff if platforms change",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Platforms = []Platform{"linux/amd64"}
return a
},
wantChanges: true,
},
{
name: "diff if pull changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Pull = true
return a
},
wantChanges: true,
},
{
name: "diff if builder changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Builder = &BuilderConfig{Name: "foo"}
return a
},
wantChanges: true,
},
{
name: "diff if tags change",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Tags = []string{"foo"}
return a
},
wantChanges: true,
},
{
name: "diff if exports change",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Exports = []Export{{Raw: "foo"}}
return a
},
wantChanges: true,
},
{
name: "diff if target changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Target = "foo"
return a
},
wantChanges: true,
},
{
name: "diff if pulling",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Pull = true
return a
},
wantChanges: true,
},
{
name: "diff if noCache changes",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.NoCache = true
return a
},
wantChanges: true,
},
{
name: "diff if labels change",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Labels = map[string]string{"foo": "bar"}
return a
},
wantChanges: true,
},
{
name: "diff if secrets change",
olds: func(*testing.T, ImageState) ImageState { return baseState },
news: func(_ *testing.T, a ImageArgs) ImageArgs {
a.Secrets = map[string]string{"foo": "bar"}
return a
},
wantChanges: true,
},
}
s := newServer(nil)
encode := func(t *testing.T, x any) resource.PropertyMap {
raw, err := mapper.New(&mapper.Opts{IgnoreMissing: true}).Encode(x)
require.NoError(t, err)
return resource.NewPropertyMapFromMap(raw)
}
for _, tt := range tests {
tt := tt
baseState := baseState
baseArgs := baseArgs
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp, err := s.Diff(provider.DiffRequest{
Urn: _fakeURN,
Olds: encode(t, tt.olds(t, baseState)),
News: encode(t, tt.news(t, baseArgs)),
})
assert.NoError(t, err)
assert.Equal(t, tt.wantChanges, resp.HasChanges, resp.DetailedDiff)
})
}
}
func TestValidateImageArgs(t *testing.T) {
t.Parallel()
t.Run("invalid inputs", func(t *testing.T) {
t.Parallel()
args := ImageArgs{
Tags: []string{"a/bad:tag:format"},
Exports: []Export{{Raw: "badexport,-"}},
Context: &BuildContext{Context: Context{Location: "./testdata"}},
Platforms: []Platform{","},
CacheFrom: []CacheFrom{{Raw: "=badcachefrom"}},
CacheTo: []CacheTo{{Raw: "=badcacheto"}},
}
_, err := args.validate(false)
assert.ErrorContains(t, err, "invalid value badexport")
assert.ErrorContains(t, err, "platform specifier component must match")
assert.ErrorContains(t, err, "badcachefrom")
assert.ErrorContains(t, err, "badcacheto")
assert.ErrorContains(t, err, "invalid reference format")
assert.ErrorContains(t, err, "testdata/Dockerfile")
})
t.Run("buildOnPreview", func(t *testing.T) {
t.Parallel()
args := ImageArgs{
Context: &BuildContext{Context: Context{Location: "testdata/noop"}},
Tags: []string{"my-tag"},
Exports: []Export{{Registry: &ExportRegistry{ExportImage{Push: pulumi.BoolRef(true)}}}},
}
actual, err := args.validate(true)
assert.NoError(t, err)
assert.Equal(t, "image", actual.Exports[0].Type)
assert.Equal(t, "false", actual.Exports[0].Attrs["push"])
actual, err = args.validate(false)
assert.NoError(t, err)
assert.Equal(t, "image", actual.Exports[0].Type)
assert.Equal(t, "true", actual.Exports[0].Attrs["push"])
})
t.Run("unknowns", func(t *testing.T) {
t.Parallel()
// pulumi-go-provider gives us zero-values when a property is unknown.
// We can't distinguish this from user-provided zero-values, but we
// should:
// - not fail previews due to these zero values,
// - not attempt builds with invalid zero values,
// - not allow invalid zero values in non-preview operations.
unknowns := ImageArgs{
BuildArgs: map[string]string{
"known": "value",
"": "",
},
Builder: nil,
CacheFrom: []CacheFrom{{GHA: &CacheFromGitHubActions{}}, {Raw: ""}},
CacheTo: []CacheTo{{GHA: &CacheToGitHubActions{}}, {Raw: ""}},
Context: nil,
Exports: []Export{{Raw: ""}},
Dockerfile: nil,
Platforms: []Platform{"linux/amd64", ""},
Registries: []Registry{
{
Address: "",
Password: "",
Username: "",
},
},
Tags: []string{"known", ""},
}
_, err := unknowns.validate(true)
assert.NoError(t, err)
assert.False(t, unknowns.buildable())
_, err = unknowns.validate(false)
assert.Error(t, err)
})
t.Run("disabled caches", func(t *testing.T) {
t.Parallel()
args := ImageArgs{
Context: &BuildContext{Context: Context{Location: "testdata/noop"}},
CacheFrom: []CacheFrom{{Raw: "type=registry", Disabled: true}},
CacheTo: []CacheTo{{Raw: "type=registry", Disabled: true}},
Exports: []Export{{Raw: "type=registry", Disabled: true}},
}
opts, err := args.validate(true)
assert.NoError(t, err)
assert.Len(t, opts.CacheTo, 0)
assert.Len(t, opts.CacheFrom, 0)
assert.Len(t, opts.Exports, 0)
opts, err = args.validate(false)
assert.NoError(t, err)
assert.Len(t, opts.CacheTo, 0)
assert.Len(t, opts.CacheFrom, 0)
assert.Len(t, opts.Exports, 0)
})
t.Run("multiple exports aren't allowed yet", func(t *testing.T) {
t.Parallel()
args := ImageArgs{
Exports: []Export{{Raw: "type=local"}, {Raw: "type=tar"}},
}
_, err := args.validate(false)
assert.ErrorContains(t, err, "multiple exports are currently unsupported")
})
t.Run("cache and export entries are union-ish", func(t *testing.T) {
t.Parallel()
args := ImageArgs{
Exports: []Export{{Tar: &ExportTar{}, Local: &ExportLocal{}}},
CacheTo: []CacheTo{{Raw: "type=tar", Local: &CacheToLocal{Dest: "/foo"}}},
CacheFrom: []CacheFrom{{Raw: "type=tar", Registry: &CacheFromRegistry{}}},
}
_, err := args.validate(false)
assert.ErrorContains(t, err, "exports should only specify one export type")
assert.ErrorContains(t, err, "cacheFrom should only specify one cache type")
assert.ErrorContains(t, err, "cacheTo should only specify one cache type")
})
t.Run("dockerfile parsing", func(t *testing.T) {
t.Parallel()
path := "./testdata/Dockerfile.invalid"
data, err := os.ReadFile(path)
require.NoError(t, err)
for _, d := range []Dockerfile{
{Location: path}, {Inline: string(data)},
} {
d := d
args := ImageArgs{Dockerfile: &d}
_, err := args.validate(false)
assert.ErrorContains(t, err, "unknown instruction: RUNN (did you mean RUN?)")
}
})
}
func TestBuildable(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args ImageArgs
want bool
}{
{
name: "unknown tags",
args: ImageArgs{Tags: []string{""}},
want: false,
},
{
name: "unknown exports",
args: ImageArgs{
Tags: []string{"known"},
Exports: []Export{{Raw: ""}},
},
want: false,
},
{
name: "unknown registry",
args: ImageArgs{
Tags: []string{"known"},
Exports: []Export{{Docker: &ExportDocker{}}},
Registries: []Registry{
{
Address: "docker.io",
Username: "foo",
Password: "",
},
},
},
want: false,
},
{
name: "known tags",
args: ImageArgs{
Tags: []string{"known"},
},
want: true,
},
{
name: "known exports",
args: ImageArgs{
Tags: []string{"known"},
Exports: []Export{{Registry: &ExportRegistry{}}},
},
want: true,
},
{
name: "known registry",
args: ImageArgs{
Tags: []string{"known"},
Exports: []Export{{Registry: &ExportRegistry{}}},
Registries: []Registry{
{
Address: "docker.io",
Username: "foo",
Password: "bar",
},
},
},
want: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := tt.args.buildable()
assert.Equal(t, tt.want, actual)
})
}
}
func TestToBuild(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
pctx := NewMockProviderContext(ctrl)
pctx.EXPECT().Log(gomock.Any(), gomock.Any()).AnyTimes()
max := Max
ia := ImageArgs{
Tags: []string{"foo", "bar"},
Platforms: []Platform{"linux/amd64"},
Context: &BuildContext{Context: Context{Location: "testdata/noop"}},
CacheTo: []CacheTo{
{GHA: &CacheToGitHubActions{CacheWithMode: CacheWithMode{&max}}},
{
Registry: &CacheToRegistry{
CacheFromRegistry: CacheFromRegistry{Ref: "docker.io/foo/bar"},
},
},
{
Registry: &CacheToRegistry{
CacheFromRegistry: CacheFromRegistry{Ref: "docker.io/foo/bar:baz"},
},
},
},
CacheFrom: []CacheFrom{
{S3: &CacheFromS3{Name: "bar"}},
{Registry: &CacheFromRegistry{Ref: "docker.io/foo/bar"}},
{Registry: &CacheFromRegistry{Ref: "docker.io/foo/bar:baz"}},
},
}
_, err := ia.toBuild(pctx, false)
assert.NoError(t, err)
}

335
provider/internal/index.go Normal file
View File

@@ -0,0 +1,335 @@
// 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(`
A wrapper around "docker buildx imagetools create" to create an index
(or manifest list) referencing one or more existing images.
In most cases you do not need an "Index" to build a multi-platform
image -- specifying multiple platforms on the "Image" will handle this
for you automatically.
However, as of April 2024, building multi-platform images _with
caching_ will only export a cache for one platform at a time (see [this
discussion](https://github.com/docker/buildx/discussions/1382) for more
details).
Therefore this resource can be helpful if you are building
multi-platform images with caching: each platform can be built and
cached separately, and an "Index" can join them all together. An
example of this is shown below.
This resource 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...)
}

View File

@@ -0,0 +1,210 @@
// 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 (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
provider "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi-go-provider/integration"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/mapper"
)
func TestIndexLifecycle(t *testing.T) {
t.Parallel()
realClient := func(t *testing.T) Client { return nil }
tests := []struct {
name string
skip bool
op func(t *testing.T) integration.Operation
client func(t *testing.T) Client
}{
{
name: "not pushed",
client: realClient,
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"tag": resource.NewStringProperty(
"docker.io/pulumibot/buildkit-e2e:manifest-unit",
),
"sources": resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty("docker.io/pulumibot/buildkit-e2e:arm64"),
resource.NewStringProperty("docker.io/pulumibot/buildkit-e2e:amd64"),
}),
"push": resource.NewBoolProperty(false),
},
}
},
},
{
name: "pushed",
skip: os.Getenv("DOCKER_HUB_PASSWORD") == "",
client: realClient,
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"tag": resource.NewStringProperty(
"docker.io/pulumibot/buildkit-e2e:manifest",
),
"sources": resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty("docker.io/pulumibot/buildkit-e2e:arm64"),
resource.NewStringProperty("docker.io/pulumibot/buildkit-e2e:amd64"),
}),
"push": resource.NewBoolProperty(true),
"registry": resource.NewObjectProperty(resource.PropertyMap{
"address": resource.NewStringProperty("docker.io"),
"username": resource.NewStringProperty("pulumibot"),
"password": resource.NewSecretProperty(&resource.Secret{
Element: resource.NewStringProperty(
os.Getenv("DOCKER_HUB_PASSWORD"),
),
}),
}),
},
}
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.skip {
t.Skip("missing environment variables")
}
lc := integration.LifeCycleTest{
Resource: "docker-build:index:Index",
Create: tt.op(t),
}
s := newServer(tt.client(t))
err := s.Configure(provider.ConfigureRequest{})
require.NoError(t, err)
lc.Run(t, s)
})
}
}
func TestIndexDiff(t *testing.T) {
t.Parallel()
urn := resource.NewURN("test", "provider", "a", "docker-build:index:Index", "test")
baseArgs := IndexArgs{Sources: []string{"docker.io/nginx:latest"}}
baseState := IndexState{IndexArgs: baseArgs}
tests := []struct {
name string
olds func(*testing.T, IndexState) IndexState
news func(*testing.T, IndexArgs) IndexArgs
wantChanges bool
}{
{
name: "no diff if no changes",
olds: func(*testing.T, IndexState) IndexState { return baseState },
news: func(*testing.T, IndexArgs) IndexArgs { return baseArgs },
wantChanges: false,
},
{
name: "diff if tag changes",
olds: func(*testing.T, IndexState) IndexState { return baseState },
news: func(t *testing.T, a IndexArgs) IndexArgs {
a.Tag = "new-tag"
return a
},
wantChanges: true,
},
{
name: "no diff if registry password changes",
olds: func(_ *testing.T, s IndexState) IndexState {
s.Registry = &Registry{
Address: "foo",
Username: "foo",
Password: "foo",
}
return s
},
news: func(_ *testing.T, a IndexArgs) IndexArgs {
a.Registry = &Registry{
Address: "foo",
Username: "foo",
Password: "DIFFERENT PASSWORD",
}
return a
},
wantChanges: false,
},
{
name: "diff if registry added",
olds: func(*testing.T, IndexState) IndexState { return baseState },
news: func(_ *testing.T, a IndexArgs) IndexArgs {
a.Registry = &Registry{Address: "foo.com", Username: "foo", Password: "foo"}
return a
},
wantChanges: true,
},
{
name: "diff if registry user changes",
olds: func(_ *testing.T, s IndexState) IndexState {
s.Registry = &Registry{
Address: "foo",
Username: "foo",
Password: "foo",
}
return s
},
news: func(_ *testing.T, a IndexArgs) IndexArgs {
a.Registry = &Registry{
Address: "DIFFERENT USER",
Username: "foo",
Password: "foo",
}
return a
},
wantChanges: true,
},
}
s := newServer(nil)
encode := func(t *testing.T, x any) resource.PropertyMap {
raw, err := mapper.New(&mapper.Opts{IgnoreMissing: true}).Encode(x)
require.NoError(t, err)
return resource.NewPropertyMapFromMap(raw)
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp, err := s.Diff(provider.DiffRequest{
Urn: urn,
Olds: encode(t, tt.olds(t, baseState)),
News: encode(t, tt.news(t, baseArgs)),
})
assert.NoError(t, err)
assert.Equal(t, tt.wantChanges, resp.HasChanges, resp.DetailedDiff)
})
}
}

View File

@@ -0,0 +1 @@
{}

737
provider/internal/mockcli_test.go generated Normal file
View File

@@ -0,0 +1,737 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: cli.go
//
// Generated by this command:
//
// mockgen -typed -package internal -source cli.go -destination mockcli_test.go --self_package github.com/pulumi/pulumi-docker-build/provider/internal
//
// Package internal is a generated GoMock package.
package internal
import (
io "io"
reflect "reflect"
command "github.com/docker/cli/cli/command"
configfile "github.com/docker/cli/cli/config/configfile"
docker "github.com/docker/cli/cli/context/docker"
store "github.com/docker/cli/cli/context/store"
store0 "github.com/docker/cli/cli/manifest/store"
client "github.com/docker/cli/cli/registry/client"
streams "github.com/docker/cli/cli/streams"
trust "github.com/docker/cli/cli/trust"
client0 "github.com/docker/docker/client"
client1 "github.com/theupdateframework/notary/client"
gomock "go.uber.org/mock/gomock"
)
// MockCli is a mock of Cli interface.
type MockCli struct {
ctrl *gomock.Controller
recorder *MockCliMockRecorder
}
// MockCliMockRecorder is the mock recorder for MockCli.
type MockCliMockRecorder struct {
mock *MockCli
}
// NewMockCli creates a new mock instance.
func NewMockCli(ctrl *gomock.Controller) *MockCli {
mock := &MockCli{ctrl: ctrl}
mock.recorder = &MockCliMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCli) EXPECT() *MockCliMockRecorder {
return m.recorder
}
// Apply mocks base method.
func (m *MockCli) Apply(ops ...command.CLIOption) error {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range ops {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Apply", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Apply indicates an expected call of Apply.
func (mr *MockCliMockRecorder) Apply(ops ...any) *CliApplyCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockCli)(nil).Apply), ops...)
return &CliApplyCall{Call: call}
}
// CliApplyCall wrap *gomock.Call
type CliApplyCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliApplyCall) Return(arg0 error) *CliApplyCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliApplyCall) Do(f func(...command.CLIOption) error) *CliApplyCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliApplyCall) DoAndReturn(f func(...command.CLIOption) error) *CliApplyCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// BuildKitEnabled mocks base method.
func (m *MockCli) BuildKitEnabled() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BuildKitEnabled")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BuildKitEnabled indicates an expected call of BuildKitEnabled.
func (mr *MockCliMockRecorder) BuildKitEnabled() *CliBuildKitEnabledCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildKitEnabled", reflect.TypeOf((*MockCli)(nil).BuildKitEnabled))
return &CliBuildKitEnabledCall{Call: call}
}
// CliBuildKitEnabledCall wrap *gomock.Call
type CliBuildKitEnabledCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliBuildKitEnabledCall) Return(arg0 bool, arg1 error) *CliBuildKitEnabledCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliBuildKitEnabledCall) Do(f func() (bool, error)) *CliBuildKitEnabledCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliBuildKitEnabledCall) DoAndReturn(f func() (bool, error)) *CliBuildKitEnabledCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Client mocks base method.
func (m *MockCli) Client() client0.APIClient {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Client")
ret0, _ := ret[0].(client0.APIClient)
return ret0
}
// Client indicates an expected call of Client.
func (mr *MockCliMockRecorder) Client() *CliClientCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Client", reflect.TypeOf((*MockCli)(nil).Client))
return &CliClientCall{Call: call}
}
// CliClientCall wrap *gomock.Call
type CliClientCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliClientCall) Return(arg0 client0.APIClient) *CliClientCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliClientCall) Do(f func() client0.APIClient) *CliClientCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliClientCall) DoAndReturn(f func() client0.APIClient) *CliClientCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ConfigFile mocks base method.
func (m *MockCli) ConfigFile() *configfile.ConfigFile {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigFile")
ret0, _ := ret[0].(*configfile.ConfigFile)
return ret0
}
// ConfigFile indicates an expected call of ConfigFile.
func (mr *MockCliMockRecorder) ConfigFile() *CliConfigFileCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigFile", reflect.TypeOf((*MockCli)(nil).ConfigFile))
return &CliConfigFileCall{Call: call}
}
// CliConfigFileCall wrap *gomock.Call
type CliConfigFileCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliConfigFileCall) Return(arg0 *configfile.ConfigFile) *CliConfigFileCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliConfigFileCall) Do(f func() *configfile.ConfigFile) *CliConfigFileCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliConfigFileCall) DoAndReturn(f func() *configfile.ConfigFile) *CliConfigFileCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ContentTrustEnabled mocks base method.
func (m *MockCli) ContentTrustEnabled() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContentTrustEnabled")
ret0, _ := ret[0].(bool)
return ret0
}
// ContentTrustEnabled indicates an expected call of ContentTrustEnabled.
func (mr *MockCliMockRecorder) ContentTrustEnabled() *CliContentTrustEnabledCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContentTrustEnabled", reflect.TypeOf((*MockCli)(nil).ContentTrustEnabled))
return &CliContentTrustEnabledCall{Call: call}
}
// CliContentTrustEnabledCall wrap *gomock.Call
type CliContentTrustEnabledCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliContentTrustEnabledCall) Return(arg0 bool) *CliContentTrustEnabledCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliContentTrustEnabledCall) Do(f func() bool) *CliContentTrustEnabledCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliContentTrustEnabledCall) DoAndReturn(f func() bool) *CliContentTrustEnabledCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ContextStore mocks base method.
func (m *MockCli) ContextStore() store.Store {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContextStore")
ret0, _ := ret[0].(store.Store)
return ret0
}
// ContextStore indicates an expected call of ContextStore.
func (mr *MockCliMockRecorder) ContextStore() *CliContextStoreCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextStore", reflect.TypeOf((*MockCli)(nil).ContextStore))
return &CliContextStoreCall{Call: call}
}
// CliContextStoreCall wrap *gomock.Call
type CliContextStoreCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliContextStoreCall) Return(arg0 store.Store) *CliContextStoreCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliContextStoreCall) Do(f func() store.Store) *CliContextStoreCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliContextStoreCall) DoAndReturn(f func() store.Store) *CliContextStoreCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// CurrentContext mocks base method.
func (m *MockCli) CurrentContext() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CurrentContext")
ret0, _ := ret[0].(string)
return ret0
}
// CurrentContext indicates an expected call of CurrentContext.
func (mr *MockCliMockRecorder) CurrentContext() *CliCurrentContextCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentContext", reflect.TypeOf((*MockCli)(nil).CurrentContext))
return &CliCurrentContextCall{Call: call}
}
// CliCurrentContextCall wrap *gomock.Call
type CliCurrentContextCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliCurrentContextCall) Return(arg0 string) *CliCurrentContextCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliCurrentContextCall) Do(f func() string) *CliCurrentContextCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliCurrentContextCall) DoAndReturn(f func() string) *CliCurrentContextCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// CurrentVersion mocks base method.
func (m *MockCli) CurrentVersion() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CurrentVersion")
ret0, _ := ret[0].(string)
return ret0
}
// CurrentVersion indicates an expected call of CurrentVersion.
func (mr *MockCliMockRecorder) CurrentVersion() *CliCurrentVersionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentVersion", reflect.TypeOf((*MockCli)(nil).CurrentVersion))
return &CliCurrentVersionCall{Call: call}
}
// CliCurrentVersionCall wrap *gomock.Call
type CliCurrentVersionCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliCurrentVersionCall) Return(arg0 string) *CliCurrentVersionCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliCurrentVersionCall) Do(f func() string) *CliCurrentVersionCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliCurrentVersionCall) DoAndReturn(f func() string) *CliCurrentVersionCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// DefaultVersion mocks base method.
func (m *MockCli) DefaultVersion() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DefaultVersion")
ret0, _ := ret[0].(string)
return ret0
}
// DefaultVersion indicates an expected call of DefaultVersion.
func (mr *MockCliMockRecorder) DefaultVersion() *CliDefaultVersionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultVersion", reflect.TypeOf((*MockCli)(nil).DefaultVersion))
return &CliDefaultVersionCall{Call: call}
}
// CliDefaultVersionCall wrap *gomock.Call
type CliDefaultVersionCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliDefaultVersionCall) Return(arg0 string) *CliDefaultVersionCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliDefaultVersionCall) Do(f func() string) *CliDefaultVersionCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliDefaultVersionCall) DoAndReturn(f func() string) *CliDefaultVersionCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// DockerEndpoint mocks base method.
func (m *MockCli) DockerEndpoint() docker.Endpoint {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DockerEndpoint")
ret0, _ := ret[0].(docker.Endpoint)
return ret0
}
// DockerEndpoint indicates an expected call of DockerEndpoint.
func (mr *MockCliMockRecorder) DockerEndpoint() *CliDockerEndpointCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DockerEndpoint", reflect.TypeOf((*MockCli)(nil).DockerEndpoint))
return &CliDockerEndpointCall{Call: call}
}
// CliDockerEndpointCall wrap *gomock.Call
type CliDockerEndpointCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliDockerEndpointCall) Return(arg0 docker.Endpoint) *CliDockerEndpointCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliDockerEndpointCall) Do(f func() docker.Endpoint) *CliDockerEndpointCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliDockerEndpointCall) DoAndReturn(f func() docker.Endpoint) *CliDockerEndpointCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Err mocks base method.
func (m *MockCli) Err() io.Writer {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Err")
ret0, _ := ret[0].(io.Writer)
return ret0
}
// Err indicates an expected call of Err.
func (mr *MockCliMockRecorder) Err() *CliErrCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockCli)(nil).Err))
return &CliErrCall{Call: call}
}
// CliErrCall wrap *gomock.Call
type CliErrCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliErrCall) Return(arg0 io.Writer) *CliErrCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliErrCall) Do(f func() io.Writer) *CliErrCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliErrCall) DoAndReturn(f func() io.Writer) *CliErrCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// In mocks base method.
func (m *MockCli) In() *streams.In {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "In")
ret0, _ := ret[0].(*streams.In)
return ret0
}
// In indicates an expected call of In.
func (mr *MockCliMockRecorder) In() *CliInCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "In", reflect.TypeOf((*MockCli)(nil).In))
return &CliInCall{Call: call}
}
// CliInCall wrap *gomock.Call
type CliInCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliInCall) Return(arg0 *streams.In) *CliInCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliInCall) Do(f func() *streams.In) *CliInCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliInCall) DoAndReturn(f func() *streams.In) *CliInCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ManifestStore mocks base method.
func (m *MockCli) ManifestStore() store0.Store {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ManifestStore")
ret0, _ := ret[0].(store0.Store)
return ret0
}
// ManifestStore indicates an expected call of ManifestStore.
func (mr *MockCliMockRecorder) ManifestStore() *CliManifestStoreCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ManifestStore", reflect.TypeOf((*MockCli)(nil).ManifestStore))
return &CliManifestStoreCall{Call: call}
}
// CliManifestStoreCall wrap *gomock.Call
type CliManifestStoreCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliManifestStoreCall) Return(arg0 store0.Store) *CliManifestStoreCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliManifestStoreCall) Do(f func() store0.Store) *CliManifestStoreCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliManifestStoreCall) DoAndReturn(f func() store0.Store) *CliManifestStoreCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// NotaryClient mocks base method.
func (m *MockCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client1.Repository, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NotaryClient", imgRefAndAuth, actions)
ret0, _ := ret[0].(client1.Repository)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NotaryClient indicates an expected call of NotaryClient.
func (mr *MockCliMockRecorder) NotaryClient(imgRefAndAuth, actions any) *CliNotaryClientCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotaryClient", reflect.TypeOf((*MockCli)(nil).NotaryClient), imgRefAndAuth, actions)
return &CliNotaryClientCall{Call: call}
}
// CliNotaryClientCall wrap *gomock.Call
type CliNotaryClientCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliNotaryClientCall) Return(arg0 client1.Repository, arg1 error) *CliNotaryClientCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliNotaryClientCall) Do(f func(trust.ImageRefAndAuth, []string) (client1.Repository, error)) *CliNotaryClientCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliNotaryClientCall) DoAndReturn(f func(trust.ImageRefAndAuth, []string) (client1.Repository, error)) *CliNotaryClientCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Out mocks base method.
func (m *MockCli) Out() *streams.Out {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Out")
ret0, _ := ret[0].(*streams.Out)
return ret0
}
// Out indicates an expected call of Out.
func (mr *MockCliMockRecorder) Out() *CliOutCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Out", reflect.TypeOf((*MockCli)(nil).Out))
return &CliOutCall{Call: call}
}
// CliOutCall wrap *gomock.Call
type CliOutCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliOutCall) Return(arg0 *streams.Out) *CliOutCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliOutCall) Do(f func() *streams.Out) *CliOutCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliOutCall) DoAndReturn(f func() *streams.Out) *CliOutCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// RegistryClient mocks base method.
func (m *MockCli) RegistryClient(arg0 bool) client.RegistryClient {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RegistryClient", arg0)
ret0, _ := ret[0].(client.RegistryClient)
return ret0
}
// RegistryClient indicates an expected call of RegistryClient.
func (mr *MockCliMockRecorder) RegistryClient(arg0 any) *CliRegistryClientCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryClient", reflect.TypeOf((*MockCli)(nil).RegistryClient), arg0)
return &CliRegistryClientCall{Call: call}
}
// CliRegistryClientCall wrap *gomock.Call
type CliRegistryClientCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliRegistryClientCall) Return(arg0 client.RegistryClient) *CliRegistryClientCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliRegistryClientCall) Do(f func(bool) client.RegistryClient) *CliRegistryClientCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliRegistryClientCall) DoAndReturn(f func(bool) client.RegistryClient) *CliRegistryClientCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ServerInfo mocks base method.
func (m *MockCli) ServerInfo() command.ServerInfo {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServerInfo")
ret0, _ := ret[0].(command.ServerInfo)
return ret0
}
// ServerInfo indicates an expected call of ServerInfo.
func (mr *MockCliMockRecorder) ServerInfo() *CliServerInfoCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerInfo", reflect.TypeOf((*MockCli)(nil).ServerInfo))
return &CliServerInfoCall{Call: call}
}
// CliServerInfoCall wrap *gomock.Call
type CliServerInfoCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliServerInfoCall) Return(arg0 command.ServerInfo) *CliServerInfoCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliServerInfoCall) Do(f func() command.ServerInfo) *CliServerInfoCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliServerInfoCall) DoAndReturn(f func() command.ServerInfo) *CliServerInfoCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// SetIn mocks base method.
func (m *MockCli) SetIn(in *streams.In) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetIn", in)
}
// SetIn indicates an expected call of SetIn.
func (mr *MockCliMockRecorder) SetIn(in any) *CliSetInCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIn", reflect.TypeOf((*MockCli)(nil).SetIn), in)
return &CliSetInCall{Call: call}
}
// CliSetInCall wrap *gomock.Call
type CliSetInCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *CliSetInCall) Return() *CliSetInCall {
c.Call = c.Call.Return()
return c
}
// Do rewrite *gomock.Call.Do
func (c *CliSetInCall) Do(f func(*streams.In)) *CliSetInCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *CliSetInCall) DoAndReturn(f func(*streams.In)) *CliSetInCall {
c.Call = c.Call.DoAndReturn(f)
return c
}

494
provider/internal/mockclient_test.go generated Normal file
View File

@@ -0,0 +1,494 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: client.go
//
// Generated by this command:
//
// mockgen -typed -package internal -source client.go -destination mockclient_test.go --self_package github.com/pulumi/pulumi-docker-build/provider/internal
//
// Package internal is a generated GoMock package.
package internal
import (
context "context"
reflect "reflect"
pb "github.com/docker/buildx/controller/pb"
client "github.com/moby/buildkit/client"
session "github.com/moby/buildkit/session"
provider "github.com/pulumi/pulumi-go-provider"
descriptor "github.com/regclient/regclient/types/descriptor"
gomock "go.uber.org/mock/gomock"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// Build mocks base method.
func (m *MockClient) Build(ctx provider.Context, b Build) (*client.SolveResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Build", ctx, b)
ret0, _ := ret[0].(*client.SolveResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Build indicates an expected call of Build.
func (mr *MockClientMockRecorder) Build(ctx, b any) *ClientBuildCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockClient)(nil).Build), ctx, b)
return &ClientBuildCall{Call: call}
}
// ClientBuildCall wrap *gomock.Call
type ClientBuildCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ClientBuildCall) Return(arg0 *client.SolveResponse, arg1 error) *ClientBuildCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ClientBuildCall) Do(f func(provider.Context, Build) (*client.SolveResponse, error)) *ClientBuildCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ClientBuildCall) DoAndReturn(f func(provider.Context, Build) (*client.SolveResponse, error)) *ClientBuildCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// BuildKitEnabled mocks base method.
func (m *MockClient) BuildKitEnabled() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BuildKitEnabled")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BuildKitEnabled indicates an expected call of BuildKitEnabled.
func (mr *MockClientMockRecorder) BuildKitEnabled() *ClientBuildKitEnabledCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildKitEnabled", reflect.TypeOf((*MockClient)(nil).BuildKitEnabled))
return &ClientBuildKitEnabledCall{Call: call}
}
// ClientBuildKitEnabledCall wrap *gomock.Call
type ClientBuildKitEnabledCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ClientBuildKitEnabledCall) Return(arg0 bool, arg1 error) *ClientBuildKitEnabledCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ClientBuildKitEnabledCall) Do(f func() (bool, error)) *ClientBuildKitEnabledCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ClientBuildKitEnabledCall) DoAndReturn(f func() (bool, error)) *ClientBuildKitEnabledCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Delete mocks base method.
func (m *MockClient) Delete(ctx context.Context, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockClientMockRecorder) Delete(ctx, id any) *ClientDeleteCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), ctx, id)
return &ClientDeleteCall{Call: call}
}
// ClientDeleteCall wrap *gomock.Call
type ClientDeleteCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ClientDeleteCall) Return(arg0 error) *ClientDeleteCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ClientDeleteCall) Do(f func(context.Context, string) error) *ClientDeleteCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ClientDeleteCall) DoAndReturn(f func(context.Context, string) error) *ClientDeleteCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Inspect mocks base method.
func (m *MockClient) Inspect(ctx context.Context, id string) ([]descriptor.Descriptor, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Inspect", ctx, id)
ret0, _ := ret[0].([]descriptor.Descriptor)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Inspect indicates an expected call of Inspect.
func (mr *MockClientMockRecorder) Inspect(ctx, id any) *ClientInspectCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Inspect", reflect.TypeOf((*MockClient)(nil).Inspect), ctx, id)
return &ClientInspectCall{Call: call}
}
// ClientInspectCall wrap *gomock.Call
type ClientInspectCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ClientInspectCall) Return(arg0 []descriptor.Descriptor, arg1 error) *ClientInspectCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ClientInspectCall) Do(f func(context.Context, string) ([]descriptor.Descriptor, error)) *ClientInspectCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ClientInspectCall) DoAndReturn(f func(context.Context, string) ([]descriptor.Descriptor, error)) *ClientInspectCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ManifestCreate mocks base method.
func (m *MockClient) ManifestCreate(ctx provider.Context, push bool, target string, refs ...string) error {
m.ctrl.T.Helper()
varargs := []any{ctx, push, target}
for _, a := range refs {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ManifestCreate", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// ManifestCreate indicates an expected call of ManifestCreate.
func (mr *MockClientMockRecorder) ManifestCreate(ctx, push, target any, refs ...any) *ClientManifestCreateCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, push, target}, refs...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ManifestCreate", reflect.TypeOf((*MockClient)(nil).ManifestCreate), varargs...)
return &ClientManifestCreateCall{Call: call}
}
// ClientManifestCreateCall wrap *gomock.Call
type ClientManifestCreateCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ClientManifestCreateCall) Return(arg0 error) *ClientManifestCreateCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ClientManifestCreateCall) Do(f func(provider.Context, bool, string, ...string) error) *ClientManifestCreateCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ClientManifestCreateCall) DoAndReturn(f func(provider.Context, bool, string, ...string) error) *ClientManifestCreateCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ManifestDelete mocks base method.
func (m *MockClient) ManifestDelete(ctx provider.Context, target string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ManifestDelete", ctx, target)
ret0, _ := ret[0].(error)
return ret0
}
// ManifestDelete indicates an expected call of ManifestDelete.
func (mr *MockClientMockRecorder) ManifestDelete(ctx, target any) *ClientManifestDeleteCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ManifestDelete", reflect.TypeOf((*MockClient)(nil).ManifestDelete), ctx, target)
return &ClientManifestDeleteCall{Call: call}
}
// ClientManifestDeleteCall wrap *gomock.Call
type ClientManifestDeleteCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ClientManifestDeleteCall) Return(arg0 error) *ClientManifestDeleteCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ClientManifestDeleteCall) Do(f func(provider.Context, string) error) *ClientManifestDeleteCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ClientManifestDeleteCall) DoAndReturn(f func(provider.Context, string) error) *ClientManifestDeleteCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ManifestInspect mocks base method.
func (m *MockClient) ManifestInspect(ctx provider.Context, target string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ManifestInspect", ctx, target)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ManifestInspect indicates an expected call of ManifestInspect.
func (mr *MockClientMockRecorder) ManifestInspect(ctx, target any) *ClientManifestInspectCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ManifestInspect", reflect.TypeOf((*MockClient)(nil).ManifestInspect), ctx, target)
return &ClientManifestInspectCall{Call: call}
}
// ClientManifestInspectCall wrap *gomock.Call
type ClientManifestInspectCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ClientManifestInspectCall) Return(arg0 string, arg1 error) *ClientManifestInspectCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ClientManifestInspectCall) Do(f func(provider.Context, string) (string, error)) *ClientManifestInspectCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ClientManifestInspectCall) DoAndReturn(f func(provider.Context, string) (string, error)) *ClientManifestInspectCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockBuild is a mock of Build interface.
type MockBuild struct {
ctrl *gomock.Controller
recorder *MockBuildMockRecorder
}
// MockBuildMockRecorder is the mock recorder for MockBuild.
type MockBuildMockRecorder struct {
mock *MockBuild
}
// NewMockBuild creates a new mock instance.
func NewMockBuild(ctrl *gomock.Controller) *MockBuild {
mock := &MockBuild{ctrl: ctrl}
mock.recorder = &MockBuildMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBuild) EXPECT() *MockBuildMockRecorder {
return m.recorder
}
// BuildOptions mocks base method.
func (m *MockBuild) BuildOptions() pb.BuildOptions {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BuildOptions")
ret0, _ := ret[0].(pb.BuildOptions)
return ret0
}
// BuildOptions indicates an expected call of BuildOptions.
func (mr *MockBuildMockRecorder) BuildOptions() *BuildBuildOptionsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildOptions", reflect.TypeOf((*MockBuild)(nil).BuildOptions))
return &BuildBuildOptionsCall{Call: call}
}
// BuildBuildOptionsCall wrap *gomock.Call
type BuildBuildOptionsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *BuildBuildOptionsCall) Return(arg0 pb.BuildOptions) *BuildBuildOptionsCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *BuildBuildOptionsCall) Do(f func() pb.BuildOptions) *BuildBuildOptionsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *BuildBuildOptionsCall) DoAndReturn(f func() pb.BuildOptions) *BuildBuildOptionsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Inline mocks base method.
func (m *MockBuild) Inline() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Inline")
ret0, _ := ret[0].(string)
return ret0
}
// Inline indicates an expected call of Inline.
func (mr *MockBuildMockRecorder) Inline() *BuildInlineCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Inline", reflect.TypeOf((*MockBuild)(nil).Inline))
return &BuildInlineCall{Call: call}
}
// BuildInlineCall wrap *gomock.Call
type BuildInlineCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *BuildInlineCall) Return(arg0 string) *BuildInlineCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *BuildInlineCall) Do(f func() string) *BuildInlineCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *BuildInlineCall) DoAndReturn(f func() string) *BuildInlineCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Secrets mocks base method.
func (m *MockBuild) Secrets() session.Attachable {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Secrets")
ret0, _ := ret[0].(session.Attachable)
return ret0
}
// Secrets indicates an expected call of Secrets.
func (mr *MockBuildMockRecorder) Secrets() *BuildSecretsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Secrets", reflect.TypeOf((*MockBuild)(nil).Secrets))
return &BuildSecretsCall{Call: call}
}
// BuildSecretsCall wrap *gomock.Call
type BuildSecretsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *BuildSecretsCall) Return(arg0 session.Attachable) *BuildSecretsCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *BuildSecretsCall) Do(f func() session.Attachable) *BuildSecretsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *BuildSecretsCall) DoAndReturn(f func() session.Attachable) *BuildSecretsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ShouldExec mocks base method.
func (m *MockBuild) ShouldExec() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ShouldExec")
ret0, _ := ret[0].(bool)
return ret0
}
// ShouldExec indicates an expected call of ShouldExec.
func (mr *MockBuildMockRecorder) ShouldExec() *BuildShouldExecCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShouldExec", reflect.TypeOf((*MockBuild)(nil).ShouldExec))
return &BuildShouldExecCall{Call: call}
}
// BuildShouldExecCall wrap *gomock.Call
type BuildShouldExecCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *BuildShouldExecCall) Return(arg0 bool) *BuildShouldExecCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *BuildShouldExecCall) Do(f func() bool) *BuildShouldExecCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *BuildShouldExecCall) DoAndReturn(f func() bool) *BuildShouldExecCall {
c.Call = c.Call.DoAndReturn(f)
return c
}

View File

@@ -0,0 +1,386 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: providercontext.go
//
// Generated by this command:
//
// mockgen -typed -package internal -source providercontext.go -destination mockprovidercontext_test.go --self_package github.com/pulumi/pulumi-docker/provider/v4/internal
//
// Package internal is a generated GoMock package.
package internal
import (
reflect "reflect"
time "time"
provider "github.com/pulumi/pulumi-go-provider"
diag "github.com/pulumi/pulumi/sdk/v3/go/common/diag"
gomock "go.uber.org/mock/gomock"
)
// MockProviderContext is a mock of ProviderContext interface.
type MockProviderContext struct {
ctrl *gomock.Controller
recorder *MockProviderContextMockRecorder
}
// MockProviderContextMockRecorder is the mock recorder for MockProviderContext.
type MockProviderContextMockRecorder struct {
mock *MockProviderContext
}
// NewMockProviderContext creates a new mock instance.
func NewMockProviderContext(ctrl *gomock.Controller) *MockProviderContext {
mock := &MockProviderContext{ctrl: ctrl}
mock.recorder = &MockProviderContextMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockProviderContext) EXPECT() *MockProviderContextMockRecorder {
return m.recorder
}
// Deadline mocks base method.
func (m *MockProviderContext) Deadline() (time.Time, bool) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Deadline")
ret0, _ := ret[0].(time.Time)
ret1, _ := ret[1].(bool)
return ret0, ret1
}
// Deadline indicates an expected call of Deadline.
func (mr *MockProviderContextMockRecorder) Deadline() *ProviderContextDeadlineCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deadline", reflect.TypeOf((*MockProviderContext)(nil).Deadline))
return &ProviderContextDeadlineCall{Call: call}
}
// ProviderContextDeadlineCall wrap *gomock.Call
type ProviderContextDeadlineCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextDeadlineCall) Return(deadline time.Time, ok bool) *ProviderContextDeadlineCall {
c.Call = c.Call.Return(deadline, ok)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextDeadlineCall) Do(f func() (time.Time, bool)) *ProviderContextDeadlineCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextDeadlineCall) DoAndReturn(f func() (time.Time, bool)) *ProviderContextDeadlineCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Done mocks base method.
func (m *MockProviderContext) Done() <-chan struct{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Done")
ret0, _ := ret[0].(<-chan struct{})
return ret0
}
// Done indicates an expected call of Done.
func (mr *MockProviderContextMockRecorder) Done() *ProviderContextDoneCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Done", reflect.TypeOf((*MockProviderContext)(nil).Done))
return &ProviderContextDoneCall{Call: call}
}
// ProviderContextDoneCall wrap *gomock.Call
type ProviderContextDoneCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextDoneCall) Return(arg0 <-chan struct{}) *ProviderContextDoneCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextDoneCall) Do(f func() <-chan struct{}) *ProviderContextDoneCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextDoneCall) DoAndReturn(f func() <-chan struct{}) *ProviderContextDoneCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Err mocks base method.
func (m *MockProviderContext) Err() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Err")
ret0, _ := ret[0].(error)
return ret0
}
// Err indicates an expected call of Err.
func (mr *MockProviderContextMockRecorder) Err() *ProviderContextErrCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockProviderContext)(nil).Err))
return &ProviderContextErrCall{Call: call}
}
// ProviderContextErrCall wrap *gomock.Call
type ProviderContextErrCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextErrCall) Return(arg0 error) *ProviderContextErrCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextErrCall) Do(f func() error) *ProviderContextErrCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextErrCall) DoAndReturn(f func() error) *ProviderContextErrCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Log mocks base method.
func (m *MockProviderContext) Log(severity diag.Severity, msg string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Log", severity, msg)
}
// Log indicates an expected call of Log.
func (mr *MockProviderContextMockRecorder) Log(severity, msg any) *ProviderContextLogCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockProviderContext)(nil).Log), severity, msg)
return &ProviderContextLogCall{Call: call}
}
// ProviderContextLogCall wrap *gomock.Call
type ProviderContextLogCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextLogCall) Return() *ProviderContextLogCall {
c.Call = c.Call.Return()
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextLogCall) Do(f func(diag.Severity, string)) *ProviderContextLogCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextLogCall) DoAndReturn(f func(diag.Severity, string)) *ProviderContextLogCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// LogStatus mocks base method.
func (m *MockProviderContext) LogStatus(severity diag.Severity, msg string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "LogStatus", severity, msg)
}
// LogStatus indicates an expected call of LogStatus.
func (mr *MockProviderContextMockRecorder) LogStatus(severity, msg any) *ProviderContextLogStatusCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogStatus", reflect.TypeOf((*MockProviderContext)(nil).LogStatus), severity, msg)
return &ProviderContextLogStatusCall{Call: call}
}
// ProviderContextLogStatusCall wrap *gomock.Call
type ProviderContextLogStatusCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextLogStatusCall) Return() *ProviderContextLogStatusCall {
c.Call = c.Call.Return()
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextLogStatusCall) Do(f func(diag.Severity, string)) *ProviderContextLogStatusCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextLogStatusCall) DoAndReturn(f func(diag.Severity, string)) *ProviderContextLogStatusCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// LogStatusf mocks base method.
func (m *MockProviderContext) LogStatusf(severity diag.Severity, msg string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{severity, msg}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "LogStatusf", varargs...)
}
// LogStatusf indicates an expected call of LogStatusf.
func (mr *MockProviderContextMockRecorder) LogStatusf(severity, msg any, args ...any) *ProviderContextLogStatusfCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{severity, msg}, args...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogStatusf", reflect.TypeOf((*MockProviderContext)(nil).LogStatusf), varargs...)
return &ProviderContextLogStatusfCall{Call: call}
}
// ProviderContextLogStatusfCall wrap *gomock.Call
type ProviderContextLogStatusfCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextLogStatusfCall) Return() *ProviderContextLogStatusfCall {
c.Call = c.Call.Return()
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextLogStatusfCall) Do(f func(diag.Severity, string, ...any)) *ProviderContextLogStatusfCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextLogStatusfCall) DoAndReturn(f func(diag.Severity, string, ...any)) *ProviderContextLogStatusfCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Logf mocks base method.
func (m *MockProviderContext) Logf(severity diag.Severity, msg string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{severity, msg}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockProviderContextMockRecorder) Logf(severity, msg any, args ...any) *ProviderContextLogfCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{severity, msg}, args...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockProviderContext)(nil).Logf), varargs...)
return &ProviderContextLogfCall{Call: call}
}
// ProviderContextLogfCall wrap *gomock.Call
type ProviderContextLogfCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextLogfCall) Return() *ProviderContextLogfCall {
c.Call = c.Call.Return()
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextLogfCall) Do(f func(diag.Severity, string, ...any)) *ProviderContextLogfCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextLogfCall) DoAndReturn(f func(diag.Severity, string, ...any)) *ProviderContextLogfCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// RuntimeInformation mocks base method.
func (m *MockProviderContext) RuntimeInformation() provider.RunInfo {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RuntimeInformation")
ret0, _ := ret[0].(provider.RunInfo)
return ret0
}
// RuntimeInformation indicates an expected call of RuntimeInformation.
func (mr *MockProviderContextMockRecorder) RuntimeInformation() *ProviderContextRuntimeInformationCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuntimeInformation", reflect.TypeOf((*MockProviderContext)(nil).RuntimeInformation))
return &ProviderContextRuntimeInformationCall{Call: call}
}
// ProviderContextRuntimeInformationCall wrap *gomock.Call
type ProviderContextRuntimeInformationCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextRuntimeInformationCall) Return(arg0 provider.RunInfo) *ProviderContextRuntimeInformationCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextRuntimeInformationCall) Do(f func() provider.RunInfo) *ProviderContextRuntimeInformationCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextRuntimeInformationCall) DoAndReturn(f func() provider.RunInfo) *ProviderContextRuntimeInformationCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Value mocks base method.
func (m *MockProviderContext) Value(key any) any {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Value", key)
ret0, _ := ret[0].(any)
return ret0
}
// Value indicates an expected call of Value.
func (mr *MockProviderContextMockRecorder) Value(key any) *ProviderContextValueCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockProviderContext)(nil).Value), key)
return &ProviderContextValueCall{Call: call}
}
// ProviderContextValueCall wrap *gomock.Call
type ProviderContextValueCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *ProviderContextValueCall) Return(arg0 any) *ProviderContextValueCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *ProviderContextValueCall) Do(f func(any) any) *ProviderContextValueCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *ProviderContextValueCall) DoAndReturn(f func(any) any) *ProviderContextValueCall {
c.Call = c.Call.DoAndReturn(f)
return c
}

View File

@@ -0,0 +1,53 @@
// 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 "github.com/pulumi/pulumi-go-provider/infer"
var _ = (infer.Enum[NetworkMode])((*NetworkMode)(nil))
// NetworkMode is the --network parameter for a build.
type NetworkMode string
const (
Default NetworkMode = "default" // Default network mode.
Host NetworkMode = "host" // Host network mode.
None NetworkMode = "none" // None or no network mode.
)
// Values returns all valid NetworkMode values for SDK generation.
func (NetworkMode) Values() []infer.EnumValue[NetworkMode] {
return []infer.EnumValue[NetworkMode]{
{
Value: Default,
Description: "The default sandbox network mode.",
},
{
Value: Host,
Description: "Host network mode.",
},
{
Value: None,
Description: "Disable network access.",
},
}
}
func (n *NetworkMode) String() string {
if n == nil {
return string(Default)
}
return string(*n)
}

View File

@@ -0,0 +1,79 @@
// 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 (
"github.com/docker/buildx/util/platformutil"
"github.com/pulumi/pulumi-go-provider/infer"
)
// These enum values are derived from
// https://github.com/docker/cli/blob/d1b88930/cli/command/manifest/util.go#L22-L51
var _ = (infer.Enum[Platform])((*Platform)(nil))
// Platform is an enum capturing all available OS/architecture targets.
type Platform string
// Values returns all valid Platform values for SDK generation.
func (Platform) Values() []infer.EnumValue[Platform] {
return []infer.EnumValue[Platform]{
{Value: "darwin/386"},
{Value: "darwin/amd64"},
{Value: "darwin/arm"},
{Value: "darwin/arm64"},
{Value: "dragonfly/amd64"},
{Value: "freebsd/386"},
{Value: "freebsd/amd64"},
{Value: "freebsd/arm"},
{Value: "linux/386"},
{Value: "linux/amd64"},
{Value: "linux/arm"},
{Value: "linux/arm64"},
{Value: "linux/mips64"},
{Value: "linux/mips64le"},
{Value: "linux/ppc64le"},
{Value: "linux/riscv64"},
{Value: "linux/s390x"},
{Value: "netbsd/386"},
{Value: "netbsd/amd64"},
{Value: "netbsd/arm"},
{Value: "openbsd/386"},
{Value: "openbsd/amd64"},
{Value: "openbsd/arm"},
{Value: "plan9/386"},
{Value: "plan9/amd64"},
{Value: "solaris/amd64"},
{Value: "windows/386"},
{Value: "windows/amd64"},
}
}
func (p Platform) String() string {
return string(p)
}
func (p Platform) validate(preview bool) (string, error) {
if preview && p == "" {
// Unknown platform during preview -- nothing to do.
return "", nil
}
_, err := platformutil.Parse([]string{string(p)})
if err != nil {
return "", err
}
return string(p), nil
}

View File

@@ -0,0 +1,117 @@
// 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"
)
// keeper decides whether an element should be included for a preview
// operation, optionally returning a mutated copy of that element.
type keeper[T any] interface {
keep(T) bool
}
// filter applies a keeper to each element, returning a new slice.
func filter[T any](k keeper[T], elems ...T) []T {
if elems == nil {
return nil
}
result := make([]T, 0, len(elems))
for _, e := range elems {
if !k.keep(e) {
continue
}
result = append(result, e)
}
return result
}
// stringKeeper preserves any non-empty string values for preview.
type stringKeeper struct{ preview bool }
func (k stringKeeper) keep(s string) bool {
if !k.preview {
return true
}
return s != ""
}
//nolint:structcheck // False positive due to generics.
type stringerKeeper[T fmt.Stringer] struct{ preview bool }
//nolint:unused // False positive due to generics.
func (k stringerKeeper[T]) keep(t T) bool {
if !k.preview {
return true
}
return stringKeeper(k).keep(t.String())
}
// registryKeeper preserves any registries with known values for address and
// password. This is imprecise and doesn't permit alternative auth strategies
// like registry tokens, email, etc.
type registryKeeper struct{ preview bool }
//nolint:unused // False positive due to generics.
func (k registryKeeper) keep(r Registry) bool {
if !k.preview {
return true
}
return r.Password != "" && r.Address != ""
}
// mapKeeper preserves map elements with known keys and values.
type mapKeeper struct{ preview bool }
func (k mapKeeper) keep(m map[string]string) map[string]string {
if !k.preview || len(m) == 0 {
return m
}
kk := stringKeeper(k)
filtered := make(map[string]string)
for key, val := range m {
if !kk.keep(key) {
continue
}
if !kk.keep(val) {
continue
}
filtered[key] = val
}
return filtered
}
type contextKeeper struct{ preview bool }
func (k contextKeeper) keep(bc *BuildContext) *BuildContext {
if !k.preview || bc == nil || len(bc.Named) == 0 {
return bc
}
named := NamedContexts{}
sk := stringKeeper(k)
for k, v := range bc.Named {
if !sk.keep(k) || !sk.keep(v.Location) {
continue
}
named[k] = v
}
return &BuildContext{
Context: Context{bc.Location},
Named: named,
}
}

View File

@@ -0,0 +1,135 @@
// 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 (
"context"
"fmt"
provider "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi-go-provider/infer"
pschema "github.com/pulumi/pulumi-go-provider/middleware/schema"
"github.com/pulumi/pulumi-java/pkg/codegen/java"
csgen "github.com/pulumi/pulumi/pkg/v3/codegen/dotnet"
gogen "github.com/pulumi/pulumi/pkg/v3/codegen/go"
tsgen "github.com/pulumi/pulumi/pkg/v3/codegen/nodejs"
pygen "github.com/pulumi/pulumi/pkg/v3/codegen/python"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
var (
_ infer.CustomConfigure = (*Config)(nil)
_ infer.Annotated = (*Config)(nil)
_ infer.Annotated = (*Registry)(nil)
)
// Config configures the buildx provider.
type Config struct {
Host string `pulumi:"host,optional"`
Registries []Registry `pulumi:"registries,optional"`
host *host
}
// _mockClientKey is used by tests to inject a mock Docker client.
var _mockClientKey struct{}
// Annotate provides user-facing descriptions and defaults for Config's fields.
func (c *Config) Annotate(a infer.Annotator) {
a.Describe(&c.Host, "The build daemon's address.")
a.SetDefault(&c.Host, "", "DOCKER_HOST")
}
// Configure validates and processes user-provided configuration values.
func (c *Config) Configure(_ provider.Context) error {
h, err := newHost(c)
if err != nil {
return fmt.Errorf("getting host: %w", err)
}
c.host = h
return nil
}
// NewBuildxProvider returns a new buildx provider.
func NewBuildxProvider() provider.Provider {
return infer.Provider(
infer.Options{
Metadata: pschema.Metadata{
DisplayName: "docker-build",
Keywords: []string{"docker", "buildkit", "buildx", "kind/native"},
Description: "A Pulumi provider for building modern Docker images with buildx and BuildKit.",
Homepage: "https://pulumi.com",
Publisher: "pulumi",
License: "Apache-2.0",
Repository: "https://github.com/pulumi/pulumi-docker-build",
LanguageMap: map[string]any{
"go": gogen.GoPackageInfo{
// GenerateResourceContainerTypes: true,
Generics: gogen.GenericsSettingSideBySide,
PackageImportAliases: map[string]string{
"github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild": "dockerbuild",
},
ImportBasePath: "github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild",
},
"csharp": csgen.CSharpPackageInfo{
PackageReferences: map[string]string{
"Pulumi": "3.*",
},
},
"java": java.PackageInfo{
BuildFiles: "gradle",
GradleNexusPublishPluginVersion: "1.1.0",
Dependencies: map[string]string{
"com.pulumi:pulumi": "0.9.9",
"com.google.code.gson:gson": "2.8.9",
"com.google.code.findbugs:jsr305": "3.0.2",
},
},
"nodejs": tsgen.NodePackageInfo{
Dependencies: map[string]string{
"@pulumi/pulumi": "^3.0.0",
},
},
"python": pygen.PackageInfo{
PyProject: struct {
Enabled bool `json:"enabled,omitempty"`
}{Enabled: true},
Requires: map[string]string{
"pulumi": ">=3.0.0,<4.0.0",
},
},
},
},
Resources: []infer.InferredResource{
infer.Resource[*Image](),
infer.Resource[*Index](),
},
ModuleMap: map[tokens.ModuleName]tokens.ModuleName{
"internal": "index",
},
Config: infer.Config[*Config](),
},
)
}
// Schema returns our package specification.
func Schema(ctx context.Context, version string) schema.PackageSpec {
p := NewBuildxProvider()
spec, err := provider.GetSchema(ctx, "docker-build", version, p)
contract.AssertNoErrorf(err, "missing schema")
return spec
}

View File

@@ -0,0 +1,82 @@
// 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 (
"context"
"testing"
"github.com/blang/semver"
"github.com/stretchr/testify/assert"
provider "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi-go-provider/infer"
"github.com/pulumi/pulumi-go-provider/integration"
mwcontext "github.com/pulumi/pulumi-go-provider/middleware/context"
)
func TestConfigure(t *testing.T) {
t.Parallel()
s := newServer(nil)
err := s.Configure(
provider.ConfigureRequest{},
)
assert.NoError(t, err)
}
// TestAnnotate sanity checks that our annotations don't panic.
func TestAnnotate(t *testing.T) {
t.Parallel()
for _, tt := range []infer.Annotated{
&Config{},
&Image{},
&ImageArgs{},
&ImageState{},
&Index{},
&IndexArgs{},
&IndexState{},
} {
tt.Annotate(annotator{})
}
}
// TestSchema sanity checks that our schema doesn't panic.
func TestSchema(t *testing.T) {
t.Parallel()
Schema(context.Background(), "v4")
}
type annotator struct{}
func (annotator) Describe(_ any, _ string) {}
func (annotator) SetDefault(_, _ any, _ ...string) {}
func (annotator) SetToken(_, _ string) {}
func newServer(client Client) integration.Server {
p := NewBuildxProvider()
// Inject a mock client if provided.
if client != nil {
p = mwcontext.Wrap(p, func(ctx provider.Context) provider.Context {
return provider.CtxWithValue(ctx, _mockClientKey, client)
})
}
return integration.NewServer("docker", semver.Version{Major: 4}, p)
}

View File

@@ -0,0 +1,27 @@
// 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.
//go:generate go run go.uber.org/mock/mockgen -typed -package internal -source providercontext.go -destination mockprovidercontext_test.go --self_package github.com/pulumi/pulumi-docker/provider/v4/internal
package internal
import (
provider "github.com/pulumi/pulumi-go-provider"
)
// ProviderContext is a workaround for
// https://github.com/pulumi/pulumi-go-provider/issues/159
type ProviderContext interface {
provider.Context
}

79
provider/internal/ssh.go Normal file
View File

@@ -0,0 +1,79 @@
// 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 (
"strings"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/util/buildflags"
"github.com/pulumi/pulumi-go-provider/infer"
)
// SSH is an SSH option.
type SSH struct {
ID string `pulumi:"id"`
Paths []string `pulumi:"paths,optional"`
}
// Annotate sets docstrings on SSH.
func (s *SSH) Annotate(a infer.Annotator) {
a.Describe(&s.ID, dedent(`
Useful for distinguishing different servers that are part of the same
build.
A value of "default" is appropriate if only dealing with a single host.
`))
a.Describe(&s.Paths, dedent(`
SSH agent socket or private keys to expose to the build under the given
identifier.
Defaults to "[$SSH_AUTH_SOCK]".
Note that your keys are **not** automatically added when using an
agent. Run "ssh-add -l" locally to confirm which public keys are
visible to the agent; these will be exposed to your build.
`))
}
// String returns a CLI-encoded value for the SSH option, or an empty string if
// its ID is not known.
func (s SSH) String() string {
if s.ID == "" {
return ""
}
r := s.ID
if len(s.Paths) > 0 {
r += "=" + strings.Join(s.Paths, ",")
}
return r
}
func (s SSH) validate() (*controllerapi.SSH, error) {
parsed, err := buildflags.ParseSSHSpecs([]string{s.String()})
if err != nil {
return nil, err
}
if len(parsed) == 0 {
return nil, nil
}
_, err = controllerapi.CreateSSH([]*controllerapi.SSH{{ID: s.ID, Paths: s.Paths}})
return parsed[0], err
}

View File

@@ -0,0 +1,52 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateSSH(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ssh SSH
wantErr string
}{
{
name: "invalid path",
ssh: SSH{ID: "foo", Paths: []string{"/not/real"}},
wantErr: "/not/real: no such file or directory",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := tt.ssh.validate()
if tt.wantErr == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.wantErr)
}
})
}
}

View File

@@ -0,0 +1,2 @@
FROM scratch
RUNN

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "Hello, World!"

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "Hello, World!"

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "Hello, World!"

View File

@@ -0,0 +1,7 @@
# Ignore everything!
*
# Except changes to source data in this set of directories:
!projects/*/src
# This should therefore IGNORE projects/*/dist

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
console.log("Pulumi 💜");

View File

@@ -0,0 +1,7 @@
# Ignore everything!
*
# Except changes to source data in this set of directories:
!projects/*/src
# This should therefore IGNORE projects/*/dist

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
console.log("💜 Pulumi (included)");

View File

@@ -0,0 +1,7 @@
# Ignore everything!
*
# Except changes to source data in this set of directories:
!projects/*/src
# This should therefore IGNORE projects/*/dist

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
console.log("Pulumi 💜");

View File

@@ -0,0 +1,3 @@
.dockerignore
ignored.txt
bar/ignored.js

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
console.log("Pulumi 💜");

View File

@@ -0,0 +1 @@
bar

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
console.log("Pulumi 💜");

View File

@@ -0,0 +1 @@
bar

View File

@@ -0,0 +1 @@
FROM scratch

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
Test

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
Test

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
Hello,

View File

@@ -0,0 +1 @@
World!

View File

@@ -0,0 +1 @@
World!

View File

@@ -0,0 +1 @@
Hello,

View File

@@ -0,0 +1 @@
World!

View File

@@ -0,0 +1 @@
Hello,

View File

@@ -0,0 +1,6 @@
# Ignore everything
*
#Unignore everything
!Dockerfile
!top-dir

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
console.log("Pulumi 💜");

View File

@@ -0,0 +1,5 @@
FROM scratch
WORKDIR /app
COPY ./bar .

View File

@@ -0,0 +1 @@
console.log("Pulumi 💜");