feat: func deploy accepts image digest in --image (#1098)

* --image can be given with digest, created parser and edited some help text to reflect this

* fixed small stuff

* tests for deploy with --image

* move parser to file, static test should be kept active

* updated some error mesgs; now prints a warning if flags not set explicitly, if set to a wrong value, return an error; updated tests to fit new error messages

* --image flag message edit

* removed warning; instead print info about disabled push a build unconditionally
This commit is contained in:
David Fridrich 2022-07-12 21:24:13 +02:00 committed by GitHub
parent 3b198cb781
commit c57af36f74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 136 additions and 1 deletions

View File

@ -63,7 +63,7 @@ that is pushed to an image registry, and finally the function's Knative service
// Flags shared with Build specifically related to building: // Flags shared with Build specifically related to building:
cmd.Flags().StringP("builder", "", "pack", "build strategy to use when creating the underlying image. Currently supported build strategies are 'pack' and 's2i'.") cmd.Flags().StringP("builder", "", "pack", "build strategy to use when creating the underlying image. Currently supported build strategies are 'pack' and 's2i'.")
cmd.Flags().StringP("builder-image", "", "", "builder image, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml (as 'builder' field) for subsequent builds. ($FUNC_BUILDER_IMAGE)") cmd.Flags().StringP("builder-image", "", "", "builder image, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml (as 'builder' field) for subsequent builds. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)") cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. (Env: $FUNC_IMAGE)")
cmd.Flags().StringP("registry", "r", GetDefaultRegistry(), "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)") cmd.Flags().StringP("registry", "r", GetDefaultRegistry(), "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
cmd.Flags().BoolP("push", "u", true, "Attempt to push the function image to registry before deploying (Env: $FUNC_PUSH)") cmd.Flags().BoolP("push", "u", true, "Attempt to push the function image to registry before deploying (Env: $FUNC_PUSH)")
cmd.Flags().StringP("platform", "", "", "Target platform to build (e.g. linux/amd64).") cmd.Flags().StringP("platform", "", "", "Target platform to build (e.g. linux/amd64).")
@ -104,11 +104,27 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
return return
} }
//if --image contains '@', validate image digest and disable build and push if not set, otherwise return an error
imageSplit := strings.Split(config.Image, "@")
imageDigestProvided := false
if len(imageSplit) == 2 {
if config, err = parseImageDigest(imageSplit, config, cmd); err != nil {
return
}
imageDigestProvided = true
}
function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace, Image: config.Image}) function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace, Image: config.Image})
if err != nil { if err != nil {
return return
} }
// save image digest if provided in --image
if imageDigestProvided {
function.ImageDigest = imageSplit[1]
}
function.Envs, _, err = mergeEnvs(function.Envs, config.EnvToUpdate, config.EnvToRemove) function.Envs, _, err = mergeEnvs(function.Envs, config.EnvToUpdate, config.EnvToRemove)
if err != nil { if err != nil {
return return
@ -500,3 +516,32 @@ func validateBuildType(buildType string) error {
} }
return nil return nil
} }
func parseImageDigest(imageSplit []string, config deployConfig, cmd *cobra.Command) (deployConfig, error) {
if !strings.HasPrefix(imageSplit[1], "sha256:") {
return config, fmt.Errorf("value '%s' in --image has invalid prefix syntax for digest (should be 'sha256:')", config.Image)
}
if len(imageSplit[1][7:]) != 64 {
return config, fmt.Errorf("sha256 hash in '%s' from --image has the wrong length (%d), should be 64", imageSplit[1], len(imageSplit[1][7:]))
}
// if --build was set but not as 'disabled', return an error
if cmd.Flags().Changed("build") && config.BuildType != "disabled" {
return config, fmt.Errorf("the --build flag '%s' is not valid when using --image with digest", config.BuildType)
}
// if the --push flag was set by a user to 'true', return an error
if cmd.Flags().Changed("push") && config.Push {
return config, fmt.Errorf("the --push flag '%v' is not valid when using --image with digest", config.Push)
}
fmt.Printf("Deploying existing image with digest %s. Build and push are disabled.\n", imageSplit[1])
config.BuildType = "disabled"
config.Push = false
config.Image = imageSplit[0]
return config, nil
}

View File

@ -2,6 +2,7 @@ package cmd
import ( import (
"context" "context"
"fmt"
"os" "os"
"testing" "testing"
@ -180,3 +181,92 @@ created: 2009-11-10 23:00:00`,
}) })
} }
} }
func Test_imageWithDigest(t *testing.T) {
tests := []struct {
name string
image string
buildType string
pushBool bool
funcFile string
errString string
}{
{
name: "valid full name with digest, expect success",
image: "docker.io/4141gauron3268/static_test_digest:latest@sha256:7d66645b0add6de7af77ef332ecd4728649a2f03b9a2716422a054805b595c4e",
errString: "",
funcFile: `name: test-func
runtime: go`,
},
{
name: "valid image name, build not 'disabled', expect error",
image: "docker.io/4141gauron3268/static_test_digest:latest@sha256:7d66645b0add6de7af77ef332ecd4728649a2f03b9a2716422a054805b595c4e",
buildType: "local",
errString: "the --build flag 'local' is not valid when using --image with digest",
funcFile: `name: test-func
runtime: go`,
},
{
name: "valid image name, --push specified, expect error",
image: "docker.io/4141gauron3268/static_test_digest:latest@sha256:7d66645b0add6de7af77ef332ecd4728649a2f03b9a2716422a054805b595c4e",
pushBool: true,
errString: "the --push flag 'true' is not valid when using --image with digest",
funcFile: `name: test-func
runtime: go`,
},
{
name: "invalid digest prefix, expect error",
image: "docker.io/4141gauron3268/static_test_digest:latest@Xsha256:7d66645b0add6de7af77ef332ecd4728649a2f03b9a2716422a054805b595c4e",
errString: "value 'docker.io/4141gauron3268/static_test_digest:latest@Xsha256:7d66645b0add6de7af77ef332ecd4728649a2f03b9a2716422a054805b595c4e' in --image has invalid prefix syntax for digest (should be 'sha256:')",
funcFile: `name: test-func
runtime: go`,
},
{
name: "invalid sha hash length(added X at the end), expect error",
image: "docker.io/4141gauron3268/static_test_digest:latest@sha256:7d66645b0add6de7af77ef332ecd4728649a2f03b9a2716422a054805b595c4eX",
errString: "sha256 hash in 'sha256:7d66645b0add6de7af77ef332ecd4728649a2f03b9a2716422a054805b595c4eX' from --image has the wrong length (65), should be 64",
funcFile: `name: test-func
runtime: go`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deployer := mock.NewDeployer()
cmd := NewDeployCmd(NewClientFactory(func() *fn.Client {
return fn.New(
fn.WithDeployer(deployer))
}))
// Set flags manually & reset after.
// Differs whether build was set via CLI (gives an error if not 'disabled')
// or not (prints just a warning)
if tt.buildType == "" {
cmd.SetArgs([]string{
fmt.Sprintf("--image=%s", tt.image),
fmt.Sprintf("--push=%t", tt.pushBool),
})
} else {
cmd.SetArgs([]string{
fmt.Sprintf("--image=%s", tt.image),
fmt.Sprintf("--build=%s", tt.buildType),
fmt.Sprintf("--push=%t", tt.pushBool),
})
}
defer cmd.ResetFlags()
// set test case's func.yaml
if err := os.WriteFile("func.yaml", []byte(tt.funcFile), os.ModePerm); err != nil {
t.Fatal(err)
}
ctx := context.TODO()
_, err := cmd.ExecuteContextC(ctx)
if err != nil {
if err := err.Error(); tt.errString != err {
t.Fatalf("Error expected to be (%v) but was (%v)", tt.errString, err)
}
}
})
}
}