Skip to content

Add logic for creating default deployer in runner #5541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pkg/skaffold/deploy/status/status_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ import (
)

var (
defaultStatusCheckDeadline = 10 * time.Minute
// DefaultStatusCheckDeadline is the default timeout for resource status checks
DefaultStatusCheckDeadline = 10 * time.Minute

// Poll period for checking set to 1 second
defaultPollPeriodInMilliseconds = 1000
Expand Down Expand Up @@ -235,7 +236,7 @@ func getDeadline(d int) time.Duration {
if d > 0 {
return time.Duration(d) * time.Second
}
return defaultStatusCheckDeadline
return DefaultStatusCheckDeadline
}

func (s statusChecker) printStatusCheckSummary(out io.Writer, r *resource.Deployment, c counter) {
Expand Down
104 changes: 104 additions & 0 deletions pkg/skaffold/runner/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package runner

import (
"context"
"errors"
"fmt"

"github.com/sirupsen/logrus"
Expand All @@ -34,6 +35,7 @@ import (
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kubectl"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kustomize"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/label"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/status"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/event"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon"
pkgkubectl "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl"
Expand All @@ -44,6 +46,7 @@ import (
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/test"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/trigger"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
)

// NewForConfig returns a new SkaffoldRunner for a SkaffoldConfig
Expand Down Expand Up @@ -233,6 +236,107 @@ func getSyncer(cfg sync.Config) sync.Syncer {
return sync.NewSyncer(cfg)
}

/*
The "default deployer" is used in `skaffold apply`, which uses a `kubectl` deployer to actuate resources
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this to pkg/skaffold/apply/deployer.go or something similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it probably should....i didn't do it here because it's a bigger refactor and i didn't want to mix organization patterns. let me go take a stab at the refactor though, new PR pending

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my comment was only scoped to the new function added. Are you talking about refactoring your other PR? This function doesn't seem to be used anywhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh sorry i misread your comment, i thought you were suggesting putting it in pkg/skaffold/deploy. i actually think that's where it should be, so the runner would make calls like deploy.GetDeployer and build.GetBuilder instead of having private getDeployer and getBuilder functions which do pre-processing and then instantiate.

IMO this pattern makes more sense, but is a bigger refactor, so rather than set an example with this PR (but have mismatched organization patterns) i decided to just put it alongside the pre-existing getDeployer function. i can move it to an apply package so it's more clear what it's used for though.

separately - this PR just adds this logic for ease of review, it's only actually used in the other PR which adds the apply command.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense. I thought it's putting too many things in new.go.

on a cluster regardless of provided deployer configuration in the skaffold.yaml.
The default deployer will honor a select set of deploy configuration from an existing skaffold.yaml:
- deploy.StatusCheckDeadlineSeconds
- deploy.Logs.Prefix
- deploy.Kubectl.Flags
- deploy.Kubectl.DefaultNamespace
- deploy.Kustomize.Flags
- deploy.Kustomize.DefaultNamespace
For a multi-config project, we do not currently support resolving conflicts between differing sets of this deploy configuration.
Therefore, in this function we do implicit validation of the provided configuration, and fail if any conflict cannot be resolved.
*/
func getDefaultDeployer(runCtx *runcontext.RunContext, labels map[string]string) (deploy.Deployer, error) {
deployCfgs := runCtx.DeployConfigs()

var kFlags *latest.KubectlFlags
var logPrefix string
var defaultNamespace *string
var kubeContext string
statusCheckTimeout := -1

for _, d := range deployCfgs {
if d.KubeContext != "" {
if kubeContext != "" {
return nil, errors.New("cannot resolve active Kubernetes context - multiple contexts configured in skaffold.yaml")
}
kubeContext = d.KubeContext
}
if d.StatusCheckDeadlineSeconds != 0 && d.StatusCheckDeadlineSeconds != int(status.DefaultStatusCheckDeadline.Seconds()) {
if statusCheckTimeout != -1 && statusCheckTimeout != d.StatusCheckDeadlineSeconds {
return nil, fmt.Errorf("found multiple status check timeouts in skaffold.yaml (not supported in `skaffold apply`): %d, %d", statusCheckTimeout, d.StatusCheckDeadlineSeconds)
}
statusCheckTimeout = d.StatusCheckDeadlineSeconds
}
if d.Logs.Prefix != "" {
if logPrefix != "" && logPrefix != d.Logs.Prefix {
return nil, fmt.Errorf("found multiple log prefixes in skaffold.yaml (not supported in `skaffold apply`): %s, %s", logPrefix, d.Logs.Prefix)
}
logPrefix = d.Logs.Prefix
}
var currentDefaultNamespace *string
var currentKubectlFlags latest.KubectlFlags
if d.KubectlDeploy != nil {
currentDefaultNamespace = d.KubectlDeploy.DefaultNamespace
currentKubectlFlags = d.KubectlDeploy.Flags
}
if d.KustomizeDeploy != nil {
currentDefaultNamespace = d.KustomizeDeploy.DefaultNamespace
currentKubectlFlags = d.KustomizeDeploy.Flags
}
if kFlags == nil {
kFlags = &currentKubectlFlags
}
if err := validateKubectlFlags(kFlags, currentKubectlFlags); err != nil {
return nil, err
}
if currentDefaultNamespace != nil {
if defaultNamespace != nil && *defaultNamespace != *currentDefaultNamespace {
return nil, fmt.Errorf("found multiple namespaces in skaffold.yaml (not supported in `skaffold apply`): %s, %s", *defaultNamespace, *currentDefaultNamespace)
}
defaultNamespace = currentDefaultNamespace
}
}
if kFlags == nil {
kFlags = &latest.KubectlFlags{}
}
k := &latest.KubectlDeploy{
Flags: *kFlags,
DefaultNamespace: defaultNamespace,
}
defaultDeployer, err := kubectl.NewDeployer(runCtx, labels, k)
if err != nil {
return nil, fmt.Errorf("instantiating default kubectl deployer: %w", err)
}
return defaultDeployer, nil
}

func validateKubectlFlags(flags *latest.KubectlFlags, additional latest.KubectlFlags) error {
errStr := "conflicting sets of kubectl deploy flags not supported in `skaffold apply` (flag: %s)"
if additional.DisableValidation != flags.DisableValidation {
return fmt.Errorf(errStr, additional.DisableValidation)
}
for _, flag := range additional.Apply {
if !util.StrSliceContains(flags.Apply, flag) {
return fmt.Errorf(errStr, flag)
}
}
for _, flag := range additional.Delete {
if !util.StrSliceContains(flags.Delete, flag) {
return fmt.Errorf(errStr, flag)
}
}
for _, flag := range additional.Global {
if !util.StrSliceContains(flags.Global, flag) {
return fmt.Errorf(errStr, flag)
}
}
return nil
}

func getDeployer(runCtx *runcontext.RunContext, labels map[string]string) (deploy.Deployer, error) {
deployerCfg := runCtx.Deployers()

Expand Down
114 changes: 114 additions & 0 deletions pkg/skaffold/runner/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,117 @@ func TestGetDeployer(tOuter *testing.T) {
}
})
}

func TestGetDefaultDeployer(tOuter *testing.T) {
testutil.Run(tOuter, "TestGetDeployer", func(t *testutil.T) {
tests := []struct {
name string
cfgs []latest.DeployType
expected *kubectl.Deployer
shouldErr bool
}{
{
name: "one config with kubectl deploy",
cfgs: []latest.DeployType{{
KubectlDeploy: &latest.KubectlDeploy{},
}},
expected: t.RequireNonNilResult(kubectl.NewDeployer(&runcontext.RunContext{
Pipelines: runcontext.NewPipelines([]latest.Pipeline{{}}),
}, nil, &latest.KubectlDeploy{
Flags: latest.KubectlFlags{},
})).(*kubectl.Deployer),
},
{
name: "one config with kubectl deploy, with flags",
cfgs: []latest.DeployType{{
KubectlDeploy: &latest.KubectlDeploy{
Flags: latest.KubectlFlags{
Apply: []string{"--foo"},
Global: []string{"--bar"},
},
},
}},
expected: t.RequireNonNilResult(kubectl.NewDeployer(&runcontext.RunContext{
Pipelines: runcontext.NewPipelines([]latest.Pipeline{{}}),
}, nil, &latest.KubectlDeploy{
Flags: latest.KubectlFlags{
Apply: []string{"--foo"},
Global: []string{"--bar"},
},
})).(*kubectl.Deployer),
},
{
name: "two kubectl configs with mismatched flags should fail",
cfgs: []latest.DeployType{
{
KubectlDeploy: &latest.KubectlDeploy{
Flags: latest.KubectlFlags{
Apply: []string{"--foo"},
},
},
},
{
KubectlDeploy: &latest.KubectlDeploy{
Flags: latest.KubectlFlags{
Apply: []string{"--bar"},
},
},
},
},
shouldErr: true,
},
{
name: "one config with helm deploy",
cfgs: []latest.DeployType{{
HelmDeploy: &latest.HelmDeploy{},
}},
expected: t.RequireNonNilResult(kubectl.NewDeployer(&runcontext.RunContext{
Pipelines: runcontext.NewPipelines([]latest.Pipeline{{}}),
}, nil, &latest.KubectlDeploy{
Flags: latest.KubectlFlags{},
})).(*kubectl.Deployer),
},
{
name: "one config with kustomize deploy",
cfgs: []latest.DeployType{{
KustomizeDeploy: &latest.KustomizeDeploy{},
}},
expected: t.RequireNonNilResult(kubectl.NewDeployer(&runcontext.RunContext{
Pipelines: runcontext.NewPipelines([]latest.Pipeline{{}}),
}, nil, &latest.KubectlDeploy{
Flags: latest.KubectlFlags{},
})).(*kubectl.Deployer),
},
}

for _, test := range tests {
testutil.Run(tOuter, test.name, func(t *testutil.T) {
pipelines := []latest.Pipeline{}
for _, cfg := range test.cfgs {
pipelines = append(pipelines, latest.Pipeline{
Deploy: latest.DeployConfig{
DeployType: cfg,
},
})
}
deployer, err := getDefaultDeployer(&runcontext.RunContext{
Pipelines: runcontext.NewPipelines(pipelines),
}, nil)

t.CheckErrorAndFailNow(test.shouldErr, err)

// if we were expecting an error, this implies that the returned deployer is nil
// this error was checked in the previous call, so if we didn't fail there (i.e. the encountered error was correct),
// then the test is finished and we can continue.
if !test.shouldErr {
t.CheckTypeEquality(&kubectl.Deployer{}, deployer)

kDeployer := deployer.(*kubectl.Deployer)
if !reflect.DeepEqual(kDeployer, test.expected) {
t.Fail()
}
}
})
}
})
}
10 changes: 10 additions & 0 deletions pkg/skaffold/runner/runcontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ func (ps Pipelines) Artifacts() []*latest.Artifact {
return artifacts
}

func (ps Pipelines) DeployConfigs() []latest.DeployConfig {
var cfgs []latest.DeployConfig
for _, p := range ps.pipelines {
cfgs = append(cfgs, p.Deploy)
}
return cfgs
}

func (ps Pipelines) Deployers() []latest.DeployType {
var deployers []latest.DeployType
for _, p := range ps.pipelines {
Expand Down Expand Up @@ -127,6 +135,8 @@ func (rc *RunContext) PortForwardResources() []*latest.PortForwardResource {

func (rc *RunContext) Artifacts() []*latest.Artifact { return rc.Pipelines.Artifacts() }

func (rc *RunContext) DeployConfigs() []latest.DeployConfig { return rc.Pipelines.DeployConfigs() }

func (rc *RunContext) Deployers() []latest.DeployType { return rc.Pipelines.Deployers() }

func (rc *RunContext) TestCases() []*latest.TestCase { return rc.Pipelines.TestCases() }
Expand Down
16 changes: 16 additions & 0 deletions testutil/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ func (t *T) CheckError(shouldErr bool, err error) {
CheckError(t.T, shouldErr, err)
}

// CheckErrorAndFailNow checks that the provided error complies with whether or not we expect an error
// and fails the test execution immediately if it does not.
// Useful for testing functions which return (obj interface{}, e error) and subsequent checks operate on `obj`
// assuming that it is not nil.
func (t *T) CheckErrorAndFailNow(shouldErr bool, err error) {
t.Helper()
CheckErrorAndFailNow(t.T, shouldErr, err)
}

// CheckErrorContains checks that an error is not nil and contains
// a given message.
func (t *T) CheckErrorContains(message string, err error) {
Expand Down Expand Up @@ -297,6 +306,13 @@ func CheckError(t *testing.T, shouldErr bool, err error) {
}
}

func CheckErrorAndFailNow(t *testing.T, shouldErr bool, err error) {
t.Helper()
if err := checkErr(shouldErr, err); err != nil {
t.Fatal(err)
}
}

func EnsureTestPanicked(t *testing.T) {
if recover() == nil {
t.Errorf("should have panicked")
Expand Down