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:
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("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().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).")
@ -104,11 +104,27 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
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})
if err != nil {
return
}
// save image digest if provided in --image
if imageDigestProvided {
function.ImageDigest = imageSplit[1]
}
function.Envs, _, err = mergeEnvs(function.Envs, config.EnvToUpdate, config.EnvToRemove)
if err != nil {
return
@ -500,3 +516,32 @@ func validateBuildType(buildType string) error {
}
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 (
"context"
"fmt"
"os"
"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)
}
}
})
}
}