Skip to content

Commit 3c699ef

Browse files
committed
feat(tofu): Add support for deploying via OpenTofu
Very simple initial config by introducing a new Deployment `tofu`. Simply uses an initialized Workspace to apply. Since this needs an extension to 4beta11 schema introduces a new version 4beta12.
1 parent 8103806 commit 3c699ef

File tree

18 files changed

+2930
-1
lines changed

18 files changed

+2930
-1
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: skaffold/v4beta12
2+
kind: Config
3+
metadata:
4+
name: tofu-test
5+
deploy:
6+
tofu:
7+
workspace: workspace
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
data "dns_a_record_set" "google" {
2+
host = "google.com"
3+
}
4+
5+
output "google_addrs" {
6+
value = join(",", data.dns_a_record_set.google.addrs)
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
terraform {
2+
required_providers {
3+
dns = {
4+
source = "hashicorp/dns"
5+
version = "3.4.2"
6+
}
7+
}
8+
}
9+
10+
provider "dns" {
11+
}

integration/tofu_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2024 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package integration
18+
19+
import (
20+
"os/exec"
21+
"strings"
22+
"testing"
23+
24+
"github.com/GoogleContainerTools/skaffold/v2/integration/skaffold"
25+
)
26+
27+
func init() {
28+
initCmd := exec.Command("tofu", "init")
29+
initCmd.Dir = "testdata/deploy-opentofu"
30+
initCmd.Run()
31+
}
32+
33+
func TestTofuDeploy(t *testing.T) {
34+
MarkIntegrationTest(t, CanRunWithoutGcp)
35+
36+
output := string(skaffold.Deploy().InDir("testdata/deploy-opentofu").RunOrFailOutput(t))
37+
38+
if !strings.Contains(output, "Apply complete!") {
39+
t.Fatal("Unexpectedly apply did not complete. Output was:", output)
40+
}
41+
42+
if !strings.Contains(output, "google_addrs =") {
43+
t.Fatal("Output variable missing. Output was:", output)
44+
}
45+
}

pkg/skaffold/deploy/tofu/cli.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
Copyright 2024 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tofu
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"io"
23+
24+
deploy "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/deploy/types"
25+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/instrumentation"
26+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/tofu"
27+
)
28+
29+
// CLI holds parameters to run tofu.
30+
type CLI struct {
31+
*tofu.CLI
32+
}
33+
34+
type Config interface {
35+
tofu.Config
36+
deploy.Config
37+
}
38+
39+
func NewCLI(cfg Config) CLI {
40+
return CLI{
41+
CLI: tofu.NewCLI(cfg),
42+
}
43+
}
44+
45+
// Apply runs `tofu apply` on a Workspace.
46+
func (c *CLI) Apply(ctx context.Context, out io.Writer) error {
47+
ctx, endTrace := instrumentation.StartTrace(ctx, "Apply", map[string]string{
48+
"AppliedBy": "tofu",
49+
})
50+
defer endTrace()
51+
52+
r, w := io.Pipe()
53+
w.Close()
54+
if err := c.Run(ctx, r, out, "apply", "-auto-approve"); err != nil {
55+
endTrace(instrumentation.TraceEndError(err))
56+
return fmt.Errorf("tofu apply: %w", err)
57+
}
58+
59+
return nil
60+
}

pkg/skaffold/deploy/tofu/tofu.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
Copyright 2024 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tofu
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"io"
23+
24+
"github.com/segmentio/textio"
25+
"go.opentelemetry.io/otel/trace"
26+
27+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/access"
28+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/debug"
29+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/deploy/label"
30+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
31+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/instrumentation"
32+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/manifest"
33+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/log"
34+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
35+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/status"
36+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/sync"
37+
)
38+
39+
// Deployer deploys Workspaces using tofu CLI.
40+
type Deployer struct {
41+
configName string
42+
43+
*latest.TofuDeploy
44+
45+
tofu CLI
46+
}
47+
48+
// NewDeployer returns a new Deployer for a DeployConfig filled
49+
// with the needed configuration for `tofu apply`
50+
func NewDeployer(cfg Config, labeller *label.DefaultLabeller, d *latest.TofuDeploy, artifacts []*latest.Artifact, configName string) (*Deployer, error) {
51+
52+
if d.Workspace == "" {
53+
return nil, fmt.Errorf("tofu needs Workspace to deploy from: %s", d.Workspace)
54+
}
55+
56+
tofu := NewCLI(cfg)
57+
58+
return &Deployer{
59+
configName: configName,
60+
TofuDeploy: d,
61+
tofu: tofu,
62+
}, nil
63+
}
64+
65+
func (k *Deployer) ConfigName() string {
66+
return k.configName
67+
}
68+
69+
// GetAccessor not supported
70+
func (k *Deployer) GetAccessor() access.Accessor {
71+
return &access.NoopAccessor{}
72+
}
73+
74+
// GetDebugger not supported.
75+
func (k *Deployer) GetDebugger() debug.Debugger {
76+
return &debug.NoopDebugger{}
77+
}
78+
79+
// GetLogger not supported.
80+
func (k *Deployer) GetLogger() log.Logger {
81+
return &log.NoopLogger{}
82+
}
83+
84+
// GetStatusMonitor not supported.
85+
func (k *Deployer) GetStatusMonitor() status.Monitor {
86+
return &status.NoopMonitor{}
87+
}
88+
89+
// GetSyncer not supported.
90+
func (k *Deployer) GetSyncer() sync.Syncer {
91+
return &sync.NoopSyncer{}
92+
}
93+
94+
// RegisterLocalImages not implemented
95+
func (k *Deployer) RegisterLocalImages(images []graph.Artifact) {
96+
}
97+
98+
// TrackBuildArtifacts not implemented
99+
func (k *Deployer) TrackBuildArtifacts(builds, deployedImages []graph.Artifact) {
100+
}
101+
102+
// Deploy runs `tofu apply` on Workspaces
103+
func (k *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Artifact, manifestsByConfig manifest.ManifestListByConfig) error {
104+
105+
var (
106+
childCtx context.Context
107+
endTrace func(...trace.SpanEndOption)
108+
)
109+
instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{
110+
"DeployerType": "tofu",
111+
})
112+
113+
childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_TofuApply")
114+
if err := k.tofu.Apply(childCtx, textio.NewPrefixWriter(out, " - ")); err != nil {
115+
endTrace(instrumentation.TraceEndError(err))
116+
return err
117+
}
118+
endTrace()
119+
return nil
120+
}
121+
122+
// HasRunnableHooks not supported
123+
func (k *Deployer) HasRunnableHooks() bool {
124+
return false
125+
}
126+
127+
// PreDeployHooks not supported
128+
func (k *Deployer) PreDeployHooks(ctx context.Context, out io.Writer) error {
129+
_, endTrace := instrumentation.StartTrace(ctx, "Deploy_PreHooks")
130+
endTrace()
131+
return nil
132+
}
133+
134+
// PostDeployHooks not supported
135+
func (k *Deployer) PostDeployHooks(ctx context.Context, out io.Writer) error {
136+
_, endTrace := instrumentation.StartTrace(ctx, "Deploy_PostHooks")
137+
endTrace()
138+
return nil
139+
}
140+
141+
// Cleanup deletes what was deployed by calling Deploy.
142+
func (k *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool, manifestsByConfig manifest.ManifestListByConfig) error {
143+
144+
instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{
145+
"DeployerType": "tofu",
146+
})
147+
if dryRun {
148+
return nil
149+
}
150+
// TODO: Implement delete
151+
152+
return nil
153+
}
154+
155+
// Dependencies lists all the files that describe what needs to be deployed.
156+
func (k *Deployer) Dependencies() ([]string, error) {
157+
return []string{}, nil
158+
}

pkg/skaffold/runner/deployer.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
kptV2 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/deploy/kpt"
3333
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/deploy/kubectl"
3434
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/deploy/label"
35+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/deploy/tofu"
3536
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/manifest"
3637
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/status"
3738
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log"
@@ -41,12 +42,20 @@ import (
4142
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util/stringslice"
4243
)
4344

45+
var (
46+
_ tofu.Config = (*deployerCtx)(nil)
47+
)
48+
4449
// deployerCtx encapsulates a given skaffold run context along with additional deployer constructs.
4550
type deployerCtx struct {
4651
*runcontext.RunContext
4752
deploy latest.DeployConfig
4853
}
4954

55+
func (d *deployerCtx) GetWorkspace() string {
56+
return d.deploy.TofuDeploy.Workspace
57+
}
58+
5059
func (d *deployerCtx) GetKubeContext() string {
5160
// if the kubeContext is not overridden by CLI flag or env. variable then use the value provided in config.
5261
if d.RunContext.IsDefaultKubeContext() && d.deploy.KubeContext != "" {
@@ -205,6 +214,14 @@ func GetDeployer(ctx context.Context, runCtx *runcontext.RunContext, labeller *l
205214
}
206215
deployers = append(deployers, deployer)
207216
}
217+
if d.TofuDeploy != nil {
218+
deployer, err := tofu.NewDeployer(dCtx, labeller, d.TofuDeploy, runCtx.Artifacts(), configName)
219+
if err != nil {
220+
return nil, err
221+
}
222+
223+
deployers = append(deployers, deployer)
224+
}
208225
}
209226

210227
if localDeploy && remoteDeploy {

pkg/skaffold/schema/latest/config.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
)
2727

2828
// This config version is not yet released, it is SAFE TO MODIFY the structs in this file.
29-
const Version string = "skaffold/v4beta12"
29+
const Version string = "skaffold/v4beta13"
3030

3131
// NewSkaffoldConfig creates a SkaffoldConfig
3232
func NewSkaffoldConfig() util.VersionedConfig {
@@ -903,6 +903,9 @@ type DeployType struct {
903903

904904
// CloudRunDeploy *alpha* deploys to Google Cloud Run using the Cloud Run v1 API.
905905
CloudRunDeploy *CloudRunDeploy `yaml:"cloudrun,omitempty"`
906+
907+
// OpenTofu *alpha* uses a client side `tofu` to deploy workspaces.
908+
TofuDeploy *TofuDeploy `yaml:"tofu,omitempty"`
906909
}
907910

908911
// CloudRunDeploy *alpha* deploys the container to Google Cloud Run.
@@ -1081,6 +1084,12 @@ type HelmPackaged struct {
10811084
AppVersion string `yaml:"appVersion,omitempty"`
10821085
}
10831086

1087+
// TofuDeploy describes a OpenTofu Workspace be deployed.
1088+
type TofuDeploy struct {
1089+
// It accepts environment variables via the go template syntax.
1090+
Workspace string `yaml:"workspace,omitempty" yamltags:"required" skaffold:"template"`
1091+
}
1092+
10841093
// LogsConfig configures how container logs are printed as a result of a deployment.
10851094
type LogsConfig struct {
10861095
// Prefix defines the prefix shown on each log line. Valid values are

0 commit comments

Comments
 (0)