Merge pull request #11708 from johngmyers/refactor-assets

Limit concurrency of asset copy tasks
This commit is contained in:
Kubernetes Prow Robot 2021-06-21 23:13:58 -07:00 committed by GitHub
commit 364fe4ca86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 244 additions and 427 deletions

1
cmd/kops/BUILD.bazel generated
View File

@ -91,7 +91,6 @@ go_library(
"//pkg/util/templater:go_default_library",
"//pkg/validation:go_default_library",
"//upup/pkg/fi:go_default_library",
"//upup/pkg/fi/assettasks:go_default_library",
"//upup/pkg/fi/cloudup:go_default_library",
"//upup/pkg/fi/cloudup/awsup:go_default_library",
"//upup/pkg/fi/utils:go_default_library",

View File

@ -1,5 +1,5 @@
/*
Copyright 2020 The Kubernetes Authors.
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,8 +22,7 @@ import (
"fmt"
"io"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/assettasks"
"k8s.io/kops/pkg/assets"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
"sigs.k8s.io/yaml"
@ -58,9 +57,6 @@ type AssetResult struct {
Files []*File `json:"files,omitempty"`
}
type copyAssetsTarget struct {
}
func NewCmdGetAssets(f *util.Factory, out io.Writer, getOptions *GetOptions) *cobra.Command {
options := GetAssetsOptions{
GetOptions: getOptions,
@ -120,7 +116,6 @@ func RunGetAssets(ctx context.Context, f *util.Factory, out io.Writer, options *
Images: make([]*Image, 0, len(updateClusterResults.ImageAssets)),
Files: make([]*File, 0, len(updateClusterResults.FileAssets)),
}
tasks := map[string]fi.Task{}
seen := map[string]bool{}
for _, imageAsset := range updateClusterResults.ImageAssets {
@ -132,24 +127,6 @@ func RunGetAssets(ctx context.Context, f *util.Factory, out io.Writer, options *
result.Images = append(result.Images, &image)
seen[image.Canonical] = true
}
if options.Copy && imageAsset.DownloadLocation != imageAsset.CanonicalLocation {
ctx := &fi.ModelBuilderContext{
Tasks: tasks,
}
copyImageTask := &assettasks.CopyImage{
Name: fi.String(imageAsset.DownloadLocation),
SourceImage: fi.String(imageAsset.CanonicalLocation),
TargetImage: fi.String(imageAsset.DownloadLocation),
Lifecycle: fi.LifecycleSync,
}
if err := ctx.EnsureTask(copyImageTask); err != nil {
return fmt.Errorf("error adding image-copy task: %v", err)
}
tasks = ctx.Tasks
}
}
seen = map[string]bool{}
@ -163,41 +140,12 @@ func RunGetAssets(ctx context.Context, f *util.Factory, out io.Writer, options *
result.Files = append(result.Files, &file)
seen[file.Canonical] = true
}
// test if the asset needs to be copied
if options.Copy && fileAsset.DownloadURL.String() != fileAsset.CanonicalURL.String() {
ctx := &fi.ModelBuilderContext{
Tasks: tasks,
}
copyFileTask := &assettasks.CopyFile{
Name: fi.String(fileAsset.CanonicalURL.String()),
TargetFile: fi.String(fileAsset.DownloadURL.String()),
SourceFile: fi.String(fileAsset.CanonicalURL.String()),
SHA: fi.String(fileAsset.SHAValue),
Lifecycle: fi.LifecycleSync,
}
if err := ctx.EnsureTask(copyFileTask); err != nil {
return fmt.Errorf("error adding file-copy task: %v", err)
}
tasks = ctx.Tasks
}
}
if options.Copy {
var options fi.RunTasksOptions
options.InitDefaults()
context, err := fi.NewContext(&copyAssetsTarget{}, updateClusterResults.Cluster, nil, nil, nil, nil, true, tasks)
err := assets.Copy(updateClusterResults.ImageAssets, updateClusterResults.FileAssets, updateClusterResults.Cluster)
if err != nil {
return fmt.Errorf("error building context: %v", err)
}
defer context.Close()
err = context.RunTasks(options)
if err != nil {
return fmt.Errorf("error running tasks: %v", err)
return err
}
}
@ -260,11 +208,3 @@ func fileOutputTable(files []*File, out io.Writer) error {
columns := []string{"CANONICAL", "DOWNLOAD", "SHA"}
return t.Render(files, out, columns...)
}
func (c copyAssetsTarget) Finish(taskMap map[string]fi.Task) error {
return nil
}
func (c copyAssetsTarget) ProcessDeletions() bool {
return false
}

18
pkg/assets/BUILD.bazel generated
View File

@ -2,10 +2,18 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["builder.go"],
srcs = [
"builder.go",
"copy.go",
"copyfile.go",
"copyimage.go",
"docker_api.go",
"docker_cli.go",
],
importpath = "k8s.io/kops/pkg/assets",
visibility = ["//visibility:public"],
deps = [
"//pkg/acls:go_default_library",
"//pkg/apis/kops:go_default_library",
"//pkg/apis/kops/util:go_default_library",
"//pkg/kubemanifest:go_default_library",
@ -14,6 +22,9 @@ go_library(
"//util/pkg/mirrors:go_default_library",
"//util/pkg/vfs:go_default_library",
"//vendor/github.com/blang/semver/v4:go_default_library",
"//vendor/github.com/docker/docker/api/types:go_default_library",
"//vendor/github.com/docker/docker/api/types/filters:go_default_library",
"//vendor/github.com/docker/docker/client:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/klog/v2:go_default_library",
],
@ -21,7 +32,10 @@ go_library(
go_test(
name = "go_default_test",
srcs = ["builder_test.go"],
srcs = [
"builder_test.go",
"copyfile_test.go",
],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
deps = [

119
pkg/assets/copy.go Normal file
View File

@ -0,0 +1,119 @@
/*
Copyright 2021 The Kubernetes Authors.
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 assets
import (
"fmt"
"sort"
"k8s.io/klog/v2"
"k8s.io/kops/pkg/apis/kops"
)
type assetTask interface {
Run() error
}
func Copy(imageAssets []*ImageAsset, fileAssets []*FileAsset, cluster *kops.Cluster) error {
tasks := map[string]assetTask{}
for _, imageAsset := range imageAssets {
if imageAsset.DownloadLocation != imageAsset.CanonicalLocation {
copyImageTask := &CopyImage{
Name: imageAsset.DownloadLocation,
SourceImage: imageAsset.CanonicalLocation,
TargetImage: imageAsset.DownloadLocation,
}
if existing, ok := tasks[copyImageTask.Name]; ok {
if existing.(*CopyImage).SourceImage != copyImageTask.SourceImage {
return fmt.Errorf("different sources for same image target %s: %s vs %s", copyImageTask.Name, copyImageTask.SourceImage, existing.(*CopyImage).SourceImage)
}
}
tasks[copyImageTask.Name] = copyImageTask
}
}
for _, fileAsset := range fileAssets {
if fileAsset.DownloadURL.String() != fileAsset.CanonicalURL.String() {
copyFileTask := &CopyFile{
Name: fileAsset.CanonicalURL.String(),
TargetFile: fileAsset.DownloadURL.String(),
SourceFile: fileAsset.CanonicalURL.String(),
SHA: fileAsset.SHAValue,
Cluster: cluster,
}
if existing, ok := tasks[copyFileTask.Name]; ok {
e, ok := existing.(*CopyFile)
if !ok {
return fmt.Errorf("different types for copy target %s", copyFileTask.Name)
}
if e.TargetFile != copyFileTask.TargetFile {
return fmt.Errorf("different targets for same file %s: %s vs %s", copyFileTask.Name, copyFileTask.TargetFile, e.TargetFile)
}
if e.SHA != copyFileTask.SHA {
return fmt.Errorf("different sha for same file %s: %s vs %s", copyFileTask.Name, copyFileTask.SHA, e.SHA)
}
}
tasks[copyFileTask.Name] = copyFileTask
}
}
ch := make(chan error, 5)
for i := 0; i < cap(ch); i++ {
ch <- nil
}
gotError := false
names := make([]string, 0, len(tasks))
for name := range tasks {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
task := tasks[name]
err := <-ch
if err != nil {
klog.Warning(err)
gotError = true
}
go func(n string, t assetTask) {
err := t.Run()
if err != nil {
err = fmt.Errorf("%s: %v", n, err)
}
ch <- err
}(name, task)
}
for i := 0; i < cap(ch); i++ {
err := <-ch
if err != nil {
klog.Warning(err)
gotError = true
}
}
close(ch)
if gotError {
return fmt.Errorf("not all assets copied successfully")
}
return nil
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package assettasks
package assets
import (
"bytes"
@ -25,27 +25,19 @@ import (
"k8s.io/klog/v2"
"k8s.io/kops/pkg/acls"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/util/pkg/hashing"
"k8s.io/kops/util/pkg/vfs"
)
// CopyFile copies an from a source file repository, to a target repository,
// typically used for highly secure clusters.
// +kops:fitask
type CopyFile struct {
Name *string
SourceFile *string
TargetFile *string
SHA *string
Lifecycle fi.Lifecycle
}
var _ fi.CompareWithID = &CopyFile{}
func (e *CopyFile) CompareWithID() *string {
// or should this be the SHA?
return e.Name
Name string
SourceFile string
TargetFile string
SHA string
Cluster *kops.Cluster
}
// fileExtensionForSHA returns the expected extension for the given hash
@ -61,74 +53,42 @@ func fileExtensionForSHA(sha string) (string, error) {
}
}
// Find attempts to find a file.
func (e *CopyFile) Find(c *fi.Context) (*CopyFile, error) {
expectedSHA := strings.TrimSpace(fi.StringValue(e.SHA))
func (e *CopyFile) Run() error {
expectedSHA := strings.TrimSpace(e.SHA)
shaExtension, err := fileExtensionForSHA(expectedSHA)
if err != nil {
return nil, err
return err
}
targetSHAFile := fi.StringValue(e.TargetFile) + shaExtension
targetSHAFile := e.TargetFile + shaExtension
targetSHABytes, err := vfs.Context.ReadFile(targetSHAFile)
if err != nil {
if os.IsNotExist(err) {
klog.V(4).Infof("unable to download: %q, assuming target file is not present, and if not present may not be an error: %v",
targetSHAFile, err)
return nil, nil
} else {
klog.V(4).Infof("unable to download: %q, %v", targetSHAFile, err)
}
klog.V(4).Infof("unable to download: %q, %v", targetSHAFile, err)
// TODO should we throw err here?
return nil, nil
}
targetSHA := string(targetSHABytes)
} else {
targetSHA := string(targetSHABytes)
if strings.TrimSpace(targetSHA) == expectedSHA {
actual := &CopyFile{
Name: e.Name,
TargetFile: e.TargetFile,
SHA: e.SHA,
SourceFile: e.SourceFile,
Lifecycle: e.Lifecycle,
if strings.TrimSpace(targetSHA) == expectedSHA {
klog.V(8).Infof("found matching target sha for file: %q", e.TargetFile)
return nil
}
klog.V(8).Infof("found matching target sha1 for file: %q", fi.StringValue(e.TargetFile))
return actual, nil
klog.V(8).Infof("did not find same file, found mismatching target sha1 for file: %q", e.TargetFile)
}
klog.V(8).Infof("did not find same file, found mismatching target sha1 for file: %q", fi.StringValue(e.TargetFile))
return nil, nil
}
// Run is the default run method.
func (e *CopyFile) Run(c *fi.Context) error {
return fi.DefaultDeltaRunMethod(e, c)
}
func (s *CopyFile) CheckChanges(a, e, changes *CopyFile) error {
if fi.StringValue(e.Name) == "" {
return fi.RequiredField("Name")
}
if fi.StringValue(e.SourceFile) == "" {
return fi.RequiredField("SourceFile")
}
if fi.StringValue(e.TargetFile) == "" {
return fi.RequiredField("TargetFile")
}
return nil
}
func (_ *CopyFile) Render(c *fi.Context, a, e, changes *CopyFile) error {
source := fi.StringValue(e.SourceFile)
target := fi.StringValue(e.TargetFile)
sourceSha := fi.StringValue(e.SHA)
source := e.SourceFile
target := e.TargetFile
sourceSha := e.SHA
klog.V(2).Infof("copying bits from %q to %q", source, target)
if err := transferFile(c, source, target, sourceSha); err != nil {
if err := transferFile(e.Cluster, source, target, sourceSha); err != nil {
return fmt.Errorf("unable to transfer %q to %q: %v", source, target, err)
}
@ -137,7 +97,7 @@ func (_ *CopyFile) Render(c *fi.Context, a, e, changes *CopyFile) error {
// transferFile downloads a file from the source location, validates the file matches the SHA,
// and uploads the file to the target location.
func transferFile(c *fi.Context, source string, target string, sha string) error {
func transferFile(cluster *kops.Cluster, source string, target string, sha string) error {
// TODO drop file to disk, as vfs reads file into memory. We load kubelet into memory for instance.
// TODO in s3 can we do a copy file ... would need to test
@ -188,21 +148,21 @@ func transferFile(c *fi.Context, source string, target string, sha string) error
}
klog.Infof("uploading %q to %q", source, objectStore)
if err := writeFile(c, uploadVFS, data); err != nil {
if err := writeFile(cluster, uploadVFS, data); err != nil {
return err
}
b := []byte(shaHash.Hex())
if err := writeFile(c, shaVFS, b); err != nil {
if err := writeFile(cluster, shaVFS, b); err != nil {
return err
}
return nil
}
func writeFile(c *fi.Context, p vfs.Path, data []byte) error {
func writeFile(cluster *kops.Cluster, p vfs.Path, data []byte) error {
acl, err := acls.GetACL(p, c.Cluster)
acl, err := acls.GetACL(p, cluster)
if err != nil {
return err
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package assettasks
package assets
import (
"testing"

73
pkg/assets/copyimage.go Normal file
View File

@ -0,0 +1,73 @@
/*
Copyright 2017 The Kubernetes Authors.
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 assets
import (
"fmt"
"k8s.io/klog/v2"
)
// CopyImage copies a docker image from a source registry, to a target registry,
// typically used for highly secure clusters.
type CopyImage struct {
Name string
SourceImage string
TargetImage string
}
func (e *CopyImage) Run() error {
api, err := newDockerAPI()
if err != nil {
return err
}
cli, err := newDockerCLI()
if err != nil {
return err
}
source := e.SourceImage
target := e.TargetImage
klog.Infof("copying docker image from %q to %q", source, target)
err = cli.pullImage(source)
if err != nil {
return fmt.Errorf("error pulling image %q: %v", source, err)
}
sourceImage, err := api.findImage(source)
if err != nil {
return fmt.Errorf("error finding image %q: %v", source, err)
}
if sourceImage == nil {
return fmt.Errorf("source image %q not found", source)
}
err = api.tagImage(sourceImage.ID, target)
if err != nil {
return fmt.Errorf("error tagging image %q: %v", source, err)
}
err = cli.pushImage(target)
if err != nil {
return fmt.Errorf("error pushing image %q: %v", target, err)
}
return nil
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package assettasks
package assets
import (
"context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package assettasks
package assets
import (
"fmt"

View File

@ -1,31 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"copyfile.go",
"copyfile_fitask.go",
"copyimage.go",
"copyimage_fitask.go",
"docker_api.go",
"docker_cli.go",
],
importpath = "k8s.io/kops/upup/pkg/fi/assettasks",
visibility = ["//visibility:public"],
deps = [
"//pkg/acls:go_default_library",
"//upup/pkg/fi:go_default_library",
"//util/pkg/hashing:go_default_library",
"//util/pkg/vfs:go_default_library",
"//vendor/github.com/docker/docker/api/types:go_default_library",
"//vendor/github.com/docker/docker/api/types/filters:go_default_library",
"//vendor/github.com/docker/docker/client:go_default_library",
"//vendor/k8s.io/klog/v2:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["copyfile_test.go"],
embed = [":go_default_library"],
)

View File

@ -1,51 +0,0 @@
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
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.
*/
// Code generated by fitask. DO NOT EDIT.
package assettasks
import (
"k8s.io/kops/upup/pkg/fi"
)
// CopyFile
var _ fi.HasLifecycle = &CopyFile{}
// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle
func (o *CopyFile) GetLifecycle() fi.Lifecycle {
return o.Lifecycle
}
// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle
func (o *CopyFile) SetLifecycle(lifecycle fi.Lifecycle) {
o.Lifecycle = lifecycle
}
var _ fi.HasName = &CopyFile{}
// GetName returns the Name of the object, implementing fi.HasName
func (o *CopyFile) GetName() *string {
return o.Name
}
// String is the stringer function for the task, producing readable output using fi.TaskAsString
func (o *CopyFile) String() string {
return fi.TaskAsString(o)
}

View File

@ -1,155 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 assettasks
import (
"fmt"
"k8s.io/klog/v2"
"k8s.io/kops/upup/pkg/fi"
)
// CopyImage copies a docker image from a source registry, to a target registry,
// typically used for highly secure clusters.
// +kops:fitask
type CopyImage struct {
Name *string
SourceImage *string
TargetImage *string
Lifecycle fi.Lifecycle
}
var _ fi.CompareWithID = &CopyImage{}
func (e *CopyImage) CompareWithID() *string {
return e.Name
}
func (e *CopyImage) Find(c *fi.Context) (*CopyImage, error) {
return nil, nil
// The problem here is that we can tag a local image with the remote tag, but there is no way to know
// if that has actually been pushed to the remote registry without doing a docker push
// The solution is probably to query the registries directly, but that is a little bit more code...
// For now, we just always do the copy; it isn't _too_ slow when things have already been pushed
//d, err := newDocker()
//if err != nil {
// return nil, err
//}
//
//source := fi.StringValue(e.SourceImage)
//target := fi.StringValue(e.TargetImage)
//
//targetImage, err := d.findImage(target)
//if err != nil {
// return nil, err
//}
//if targetImage == nil {
// klog.V(4).Infof("target image %q not found", target)
// return nil, nil
//}
//
//// We want to verify that the target image matches
//if err := d.pullImage(source); err != nil {
// return nil, err
//}
//
//sourceImage, err := d.findImage(source)
//if err != nil {
// return nil, err
//}
//if sourceImage == nil {
// return nil, fmt.Errorf("source image %q not found", source)
//}
//
//if sourceImage.ID == targetImage.ID {
// actual := &CopyImage{}
// actual.Name = e.Name
// actual.SourceImage = e.SourceImage
// actual.TargetImage = e.TargetImage
// klog.Infof("Found image %q = %s", target, sourceImage.ID)
// return actual, nil
//}
//
//klog.V(2).Infof("Target image %q does not match source %q: %q vs %q",
// target, source,
// targetImage.ID, sourceImage.ID)
//
//return nil, nil
}
func (e *CopyImage) Run(c *fi.Context) error {
return fi.DefaultDeltaRunMethod(e, c)
}
func (s *CopyImage) CheckChanges(a, e, changes *CopyImage) error {
if fi.StringValue(e.Name) == "" {
return fi.RequiredField("Name")
}
if fi.StringValue(e.SourceImage) == "" {
return fi.RequiredField("SourceImage")
}
if fi.StringValue(e.TargetImage) == "" {
return fi.RequiredField("TargetImage")
}
return nil
}
func (_ *CopyImage) Render(c *fi.Context, a, e, changes *CopyImage) error {
api, err := newDockerAPI()
if err != nil {
return err
}
cli, err := newDockerCLI()
if err != nil {
return err
}
source := fi.StringValue(e.SourceImage)
target := fi.StringValue(e.TargetImage)
klog.Infof("copying docker image from %q to %q", source, target)
err = cli.pullImage(source)
if err != nil {
return fmt.Errorf("error pulling image %q: %v", source, err)
}
sourceImage, err := api.findImage(source)
if err != nil {
return fmt.Errorf("error finding image %q: %v", source, err)
}
if sourceImage == nil {
return fmt.Errorf("source image %q not found", source)
}
err = api.tagImage(sourceImage.ID, target)
if err != nil {
return fmt.Errorf("error tagging image %q: %v", source, err)
}
err = cli.pushImage(target)
if err != nil {
return fmt.Errorf("error pushing image %q: %v", target, err)
}
return nil
}

View File

@ -1,51 +0,0 @@
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
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.
*/
// Code generated by fitask. DO NOT EDIT.
package assettasks
import (
"k8s.io/kops/upup/pkg/fi"
)
// CopyImage
var _ fi.HasLifecycle = &CopyImage{}
// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle
func (o *CopyImage) GetLifecycle() fi.Lifecycle {
return o.Lifecycle
}
// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle
func (o *CopyImage) SetLifecycle(lifecycle fi.Lifecycle) {
o.Lifecycle = lifecycle
}
var _ fi.HasName = &CopyImage{}
// GetName returns the Name of the object, implementing fi.HasName
func (o *CopyImage) GetName() *string {
return o.Name
}
// String is the stringer function for the task, producing readable output using fi.TaskAsString
func (o *CopyImage) String() string {
return fi.TaskAsString(o)
}