Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 17 additions & 4 deletions internal/cli/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func NewDeployCmd() *cobra.Command {
flags.StringVar(&deployOpts.manualReason, "reason", "", "Why was this deployment triggered.")
flags.BoolVar(&forceApply, "force_apply", false, "Force apply resources, overriding field manager conflicts.")
flags.MarkHidden("force_apply")
flags.BoolVar(&deployOpts.provisionOnly, "provision_only", false, "If set, provision resources (e.g. database schema) without rolling out the requested servers.")
}).
With(
fncobra.ParseEnv(&env),
Expand Down Expand Up @@ -121,7 +122,12 @@ func NewDeployCmd() *cobra.Command {
}
}

plan, err := deploy.PrepareDeployStack(ctx, p, stack)
var deployMode deploy.PrepareDeployMode
if deployOpts.provisionOnly {
deployMode = deploy.PrepareProvisionOnly
}

plan, err := deploy.PrepareDeployStackOpts(ctx, p, stack, deploy.PrepareDeployOptions{Mode: deployMode})
if err != nil {
return err
}
Expand Down Expand Up @@ -162,9 +168,10 @@ func NewDeployCmd() *cobra.Command {
}

type deployOpts struct {
alsoWait bool
outputPath string
manualReason string
alsoWait bool
outputPath string
manualReason string
provisionOnly bool
}

type Output struct {
Expand Down Expand Up @@ -231,6 +238,12 @@ func completeDeployment(ctx context.Context, env cfg.Context, cluster runtime.Cl
}
}

if opts.provisionOnly {
fmt.Fprintf(out, "\n Resources provisioned. Run %s to roll out the servers.\n",
colors.Ctx(ctx).Highlight.Apply("ns deploy"))
return nil
}

envLabel := fmt.Sprintf("--env=%s", env.Environment().Name)

fmt.Fprintf(out, "\n Next steps:\n\n")
Expand Down
47 changes: 43 additions & 4 deletions internal/planning/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,35 @@ func PrepareDeployServers(ctx context.Context, planner planning.Planner, focus .
return PrepareDeployStack(ctx, planner, stack)
}

// PrepareDeployMode selects what a deployment plan should act upon.
type PrepareDeployMode int

const (
// PrepareFullDeploy provisions resources and rolls out the requested servers.
PrepareFullDeploy PrepareDeployMode = iota
// PrepareProvisionOnly provisions resources (and the servers they require, e.g.
// a colocated database) but does not roll out the requested servers or ingress.
PrepareProvisionOnly
)

// PrepareDeployOptions tunes how a deployment plan is assembled.
type PrepareDeployOptions struct {
Mode PrepareDeployMode
}

func PrepareDeployStack(ctx context.Context, planner planning.Planner, stack *planning.Stack, prepared ...compute.Computable[PreparedDeployable]) (compute.Computable[*Plan], error) {
return PrepareDeployStackOpts(ctx, planner, stack, PrepareDeployOptions{}, prepared...)
}

func PrepareDeployStackOpts(ctx context.Context, planner planning.Planner, stack *planning.Stack, opts PrepareDeployOptions, prepared ...compute.Computable[PreparedDeployable]) (compute.Computable[*Plan], error) {
def, err := prepareHandlerInvocations(ctx, planner, stack)
if err != nil {
return nil, err
}

ingressResult := computeIngressWithHandlerResult(planner, stack, ingressesFromHandlerResult(def))

prepare, err := prepareBuildAndDeployment(ctx, planner, stack, def, ingressResult, prepared...)
prepare, err := prepareBuildAndDeployment(ctx, planner, stack, opts, def, ingressResult, prepared...)
if err != nil {
return nil, err
}
Expand All @@ -110,7 +130,7 @@ func PrepareDeployStack(ctx context.Context, planner planning.Planner, stack *pl
ingressFragments: fragmentsOnly,
}

if AlsoDeployIngress {
if AlsoDeployIngress && opts.Mode != PrepareProvisionOnly {
g.ingressPlan = PlanIngressDeployment(planner.Runtime, ingressResult)
}

Expand Down Expand Up @@ -151,7 +171,10 @@ func (m *makeDeployGraph) Inputs() *compute.In {
in := compute.Inputs().Computable("prepare", m.prepare).Indigestible("stack", m.stack)
// TODO predeploy orchestration server already from here?
if m.ingressFragments != nil {
in = in.Computable("ingress", m.ingressFragments).Computable("ingressPlan", m.ingressPlan)
in = in.Computable("ingress", m.ingressFragments)
}
if m.ingressPlan != nil {
in = in.Computable("ingressPlan", m.ingressPlan)
}
return in
}
Expand Down Expand Up @@ -220,7 +243,7 @@ type prepareAndBuildResult struct {
NamespaceReference string
}

func prepareBuildAndDeployment(ctx context.Context, planner planning.Planner, stack *planning.Stack, stackDef compute.Computable[*handlerResult], ingress compute.Computable[*ComputeIngressResult], prepared ...compute.Computable[PreparedDeployable]) (compute.Computable[prepareAndBuildResult], error) {
func prepareBuildAndDeployment(ctx context.Context, planner planning.Planner, stack *planning.Stack, opts PrepareDeployOptions, stackDef compute.Computable[*handlerResult], ingress compute.Computable[*ComputeIngressResult], prepared ...compute.Computable[PreparedDeployable]) (compute.Computable[prepareAndBuildResult], error) {
packages, images, err := computeStackAndImages(ctx, planner, stack, serverImagesOpts{
ProvisionResult: stackDef,
IngressFragments: ingress,
Expand Down Expand Up @@ -323,6 +346,12 @@ func prepareBuildAndDeployment(ctx context.Context, planner planning.Planner, st
deploymentSpec.Specs = append(deploymentSpec.Specs, spec)
}

if opts.Mode == PrepareProvisionOnly {
// Provision resources without rolling out the requested servers; keep
// only the servers that resources require (e.g. a colocated database).
deploymentSpec.Specs = keepProvisionOnlyServers(deploymentSpec.Specs, resourcePlan.RequiredServers)
}

deploymentPlan, err := planner.Runtime.PlanDeployment(ctx, deploymentSpec)
if err != nil {
return prepareAndBuildResult{}, err
Expand All @@ -335,6 +364,16 @@ func prepareBuildAndDeployment(ctx context.Context, planner planning.Planner, st
ops = append(ops, resourcePlan.ExecutionInvocations...)
ops = append(ops, deploymentPlan.Definitions...)

if opts.Mode == PrepareProvisionOnly {
sink, err := provisionOnlyOutputSink(resourcePlan.ProducedInstanceIDs)
if err != nil {
return prepareAndBuildResult{}, err
}
if sink != nil {
ops = append(ops, sink)
}
}

return prepareAndBuildResult{
HandlerResult: compute.MustGetDepValue(deps, stackDef, "stackAndDefs"),
ResourcePlan: resourcePlan,
Expand Down
23 changes: 23 additions & 0 deletions internal/planning/deploy/op_consumeresourceoutputs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2022 Namespace Labs Inc; All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.

package deploy

import (
"context"

"namespacelabs.dev/foundation/internal/resources"
"namespacelabs.dev/foundation/schema"
"namespacelabs.dev/foundation/std/execution"
)

// register_OpConsumeResourceOutputs registers a no-op handler that consumes
// resource instance outputs. The outputs are declared via RequiredOutput on the
// invocation, so the handler itself does nothing; it only exists to balance the
// plan's output accounting in provision-only deployments.
func register_OpConsumeResourceOutputs() {
execution.RegisterHandlerFunc(func(ctx context.Context, inv *schema.SerializedInvocation, op *resources.OpConsumeResourceOutputs) (*execution.HandleResult, error) {
return nil, nil
})
}
60 changes: 60 additions & 0 deletions internal/planning/deploy/provisiononly.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2022 Namespace Labs Inc; All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.

package deploy

import (
"google.golang.org/protobuf/types/known/anypb"
internalresources "namespacelabs.dev/foundation/internal/resources"
"namespacelabs.dev/foundation/internal/runtime"
"namespacelabs.dev/foundation/schema"
stdresources "namespacelabs.dev/foundation/std/resources"
)

// keepProvisionOnlyServers filters server deployables down to the ones required
// to provision resources (e.g. a colocated database). The requested servers are
// not rolled out in provision-only mode.
func keepProvisionOnlyServers(specs []runtime.DeployableSpec, required []schema.PackageName) []runtime.DeployableSpec {
keep := make(map[schema.PackageName]struct{}, len(required))
for _, p := range required {
keep[p] = struct{}{}
}

var out []runtime.DeployableSpec
for _, spec := range specs {
if spec.PackageRef == nil {
continue
}
if _, ok := keep[spec.PackageRef.AsPackageName()]; ok {
out = append(out, spec)
}
}
return out
}

// provisionOnlyOutputSink returns a no-op invocation that consumes the given
// resource instance outputs, so the plan's output accounting stays balanced when
// the requested servers (the usual consumers) are not rolled out.
func provisionOnlyOutputSink(instanceIDs []string) (*schema.SerializedInvocation, error) {
if len(instanceIDs) == 0 {
return nil, nil
}

wrapped, err := anypb.New(&internalresources.OpConsumeResourceOutputs{ResourceInstanceId: instanceIDs})
if err != nil {
return nil, err
}

after := make([]string, len(instanceIDs))
for i, id := range instanceIDs {
after[i] = stdresources.ResourceInstanceCategory(id)
}

return &schema.SerializedInvocation{
Description: "Consume resource outputs (provision-only)",
Impl: wrapped,
RequiredOutput: instanceIDs,
Order: &schema.ScheduleOrder{SchedAfterCategory: after},
}, nil
}
59 changes: 59 additions & 0 deletions internal/planning/deploy/provisiononly_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2022 Namespace Labs Inc; All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.

package deploy

import (
"testing"

"gotest.tools/assert"
internalresources "namespacelabs.dev/foundation/internal/resources"
"namespacelabs.dev/foundation/internal/runtime"
"namespacelabs.dev/foundation/schema"
stdresources "namespacelabs.dev/foundation/std/resources"
)

func TestKeepProvisionOnlyServers(t *testing.T) {
app := schema.MakePackageSingleRef("example.com/app")
pg := schema.MakePackageSingleRef("example.com/postgres")
other := schema.MakePackageSingleRef("example.com/other")

specs := []runtime.DeployableSpec{
{PackageRef: app},
{PackageRef: pg},
{PackageRef: other},
{PackageRef: nil},
}

kept := keepProvisionOnlyServers(specs, []schema.PackageName{pg.AsPackageName()})

assert.Equal(t, len(kept), 1)
assert.Equal(t, kept[0].PackageRef.AsPackageName(), pg.AsPackageName())
}

func TestProvisionOnlyOutputSinkEmpty(t *testing.T) {
sink, err := provisionOnlyOutputSink(nil)
assert.NilError(t, err)
assert.Assert(t, sink == nil)
}

func TestProvisionOnlyOutputSink(t *testing.T) {
ids := []string{"res-a", "res-b"}

sink, err := provisionOnlyOutputSink(ids)
assert.NilError(t, err)
assert.Assert(t, sink != nil)

// Every produced output must be consumed to keep the plan's accounting balanced.
assert.DeepEqual(t, sink.RequiredOutput, ids)

assert.DeepEqual(t, sink.Order.SchedAfterCategory, []string{
stdresources.ResourceInstanceCategory("res-a"),
stdresources.ResourceInstanceCategory("res-b"),
})

msg := &internalresources.OpConsumeResourceOutputs{}
assert.NilError(t, sink.Impl.UnmarshalTo(msg))
assert.DeepEqual(t, msg.ResourceInstanceId, ids)
}
1 change: 1 addition & 0 deletions internal/planning/deploy/resourceops.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ package deploy
func RegisterDeployOps() {
register_OpWaitForProviderResults()
register_OpCaptureServerConfig()
register_OpConsumeResourceOutputs()
}
13 changes: 13 additions & 0 deletions internal/planning/deploy/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ type resourcePlan struct {
PlannedResources []plannedResource
ExecutionInvocations []*schema.SerializedInvocation
Secrets []runtime.SecretResourceDependency

// ProducedInstanceIDs are the resource instance ids that produce an execution
// output (resource providers and server-resource captures). Used to wire up the
// provision-only output sink.
ProducedInstanceIDs []string
// RequiredServers are servers referenced as resources (e.g. a colocated
// database) that must be deployed even in provision-only mode.
RequiredServers []schema.PackageName
}

type plannedResource struct {
Expand Down Expand Up @@ -121,6 +129,9 @@ func planResources(ctx context.Context, planner planning.Planner, stack *plannin
return nil, fnerrors.InternalError("%s: target server is not in the stack", serverIntent.PackageName)
}

plan.ProducedInstanceIDs = append(plan.ProducedInstanceIDs, resource.ID)
plan.RequiredServers = append(plan.RequiredServers, target.PackageName())

si := &schema.SerializedInvocation{
Description: "Capture Runtime Config",
Order: &schema.ScheduleOrder{
Expand Down Expand Up @@ -218,6 +229,8 @@ func planResources(ctx context.Context, planner planning.Planner, stack *plannin
p.BinaryConfig = config
p.SerializedIntentJson = resource.JSONSerializedIntent

plan.ProducedInstanceIDs = append(plan.ProducedInstanceIDs, resource.ID)

executionInvocations = append(executionInvocations, p)

default:
Expand Down
Loading
Loading