Skip to content

Commit e60d5fc

Browse files
authored
feat(harness): add ansible support to harness (#210)
* feat(harness): add support for ansible * make sure that exec pre start hook runs before step timeout countdown starts * fix lint * simplify logic * add entrypoint to ansible image * override ansible home during docker build * create nonroot group in ansible dockerfile * simplify ansible dockerfile * ensure ansible home/tmp env vars are set * fix lint
1 parent c7ba7ea commit e60d5fc

File tree

17 files changed

+308
-99
lines changed

17 files changed

+308
-99
lines changed

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,16 @@ docker-build-harness-terraform: docker-build-harness-base ## build terraform doc
9494
-f dockerfiles/harness/terraform.Dockerfile \
9595
.
9696

97+
.PHONY: docker-build-harness-ansible
98+
docker-build-harness-ansible: docker-build-harness-base ## build terraform docker harness image
99+
docker build \
100+
--build-arg=HARNESS_IMAGE_TAG="latest" \
101+
-t harness \
102+
-f dockerfiles/harness/ansible.Dockerfile \
103+
.
104+
97105
.PHONY: docker-run-harness
98-
docker-run-harness: docker-build-harness-terraform ## build and run terraform docker harness image
106+
docker-run-harness: docker-build-harness-terraform docker-build-harness-ansible ## build and run terraform docker harness image
99107
docker run \
100108
harness:latest \
101109
--v=5 \

dockerfiles/harness/ansible.Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ FROM ${HARNESS_BASE_IMAGE} as harness
1010
# Build Ansible from Python Image
1111
FROM python:${PYTHON_VERSION}-alpine as final
1212

13-
# Create necessary directories and set their ownership to UID/GID 65532
14-
RUN mkdir /plural && chown -R 65532:65532 /plural
15-
RUN mkdir /tmp/plural && chown -R 65532:65532 /tmp/plural
16-
1713
# Copy Harness bin from the Harness Image
1814
COPY --from=harness /harness /usr/local/bin/harness
1915

@@ -33,7 +29,11 @@ RUN apk add --no-cache --virtual .build-deps \
3329
apk add --no-cache openssh-client && \
3430
apk del .build-deps
3531

32+
RUN addgroup --gid 65532 nonroot
33+
3634
# Switch to the non-root user
3735
USER 65532:65532
3836

3937
WORKDIR /plural
38+
39+
ENTRYPOINT ["harness", "--working-dir=/plural"]

pkg/harness/controller/controller.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"cmp"
55
"context"
66
"fmt"
7+
"io"
78
"path"
89
"slices"
910
"sync"
@@ -114,21 +115,25 @@ func (in *stackRunController) toExecutable(ctx context.Context, step *gqlclient.
114115
)...,
115116
)
116117

118+
var toolWriters []io.WriteCloser
117119
modifier := in.tool.Modifier(step.Stage)
118120
args := step.Args
121+
env := in.stackRun.Env()
119122
if modifier != nil {
120123
args = modifier.Args(args)
124+
env = modifier.Env(env)
125+
toolWriters = modifier.WriteCloser()
121126
}
122127

123128
return exec.NewExecutable(
124129
step.Cmd,
125130
append(
126131
in.execOptions,
127132
exec.WithDir(in.execWorkDir()),
128-
exec.WithEnv(in.stackRun.Env()),
133+
exec.WithEnv(env),
129134
exec.WithArgs(args),
130135
exec.WithID(step.ID),
131-
exec.WithLogSink(consoleWriter),
136+
exec.WithOutputSinks(append(toolWriters, consoleWriter)...),
132137
exec.WithHook(v1.LifecyclePreStart, in.preExecHook(step.Stage, step.ID)),
133138
exec.WithHook(v1.LifecyclePostStart, in.postExecHook(step.Stage)),
134139
)...,
@@ -192,7 +197,7 @@ func (in *stackRunController) prepare() error {
192197
return err
193198
}
194199

195-
in.tool = tool.New(in.stackRun.Type, in.execWorkDir())
200+
in.tool = tool.New(in.stackRun.Type, in.dir, in.execWorkDir())
196201

197202
return nil
198203
}

pkg/harness/exec/exec.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os/exec"
99
"strings"
1010

11+
"github.com/pluralsh/polly/algorithms"
1112
"k8s.io/apimachinery/pkg/util/uuid"
1213
"k8s.io/klog/v2"
1314

@@ -17,10 +18,14 @@ import (
1718
)
1819

1920
func (in *executable) Run(ctx context.Context) error {
21+
if err := in.runLifecycleFunction(v1.LifecyclePreStart); err != nil {
22+
return err
23+
}
24+
2025
ctx = signals.NewCancelableContext(ctx, signals.NewTimeoutSignal(in.timeout))
2126
cmd := exec.CommandContext(ctx, in.command, in.args...)
2227
w := in.writer()
23-
defer in.close(in.logSink)
28+
defer in.close(in.outputSinks)
2429

2530
// Configure additional writers so that we can simultaneously write output
2631
// to multiple destinations
@@ -40,10 +45,6 @@ func (in *executable) Run(ctx context.Context) error {
4045
cmd.Dir = in.workingDirectory
4146
}
4247

43-
if err := in.runLifecycleFunction(v1.LifecyclePreStart); err != nil {
44-
return err
45-
}
46-
4748
klog.V(log.LogLevelExtended).InfoS("executing", "command", in.Command())
4849
if err := cmd.Run(); err != nil {
4950
if err = context.Cause(ctx); err != nil {
@@ -86,19 +87,28 @@ func (in *executable) ID() string {
8687
}
8788

8889
func (in *executable) writer() io.Writer {
89-
if in.logSink != nil {
90-
return io.MultiWriter(os.Stdout, in.logSink)
90+
if len(in.outputSinks) == 0 {
91+
return os.Stdout
9192
}
92-
return os.Stdout
93+
94+
return io.MultiWriter(
95+
append(
96+
algorithms.Map(in.outputSinks, func(writer io.WriteCloser) io.Writer {
97+
return writer
98+
}),
99+
os.Stdout)...,
100+
)
93101
}
94102

95-
func (in *executable) close(w io.WriteCloser) {
96-
if w == nil {
103+
func (in *executable) close(writers []io.WriteCloser) {
104+
if len(writers) == 0 {
97105
return
98106
}
99107

100-
if err := w.Close(); err != nil {
101-
klog.ErrorS(err, "failed to close writer")
108+
for _, w := range writers {
109+
if err := w.Close(); err != nil {
110+
klog.ErrorS(err, "failed to close writer")
111+
}
102112
}
103113
}
104114

pkg/harness/exec/exec_options.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"io"
55
"time"
66

7+
"github.com/pluralsh/polly/algorithms"
8+
79
v1 "github.com/pluralsh/deployment-operator/pkg/harness/stackrun/v1"
810
)
911

@@ -13,9 +15,9 @@ func WithDir(workingDirectory string) Option {
1315
}
1416
}
1517

16-
func WithLogSink(sink io.WriteCloser) Option {
18+
func WithOutputSinks(sinks ...io.WriteCloser) Option {
1719
return func(e *executable) {
18-
e.logSink = sink
20+
e.outputSinks = algorithms.Filter(sinks, func(sink io.WriteCloser) bool { return sink != nil })
1921
}
2022
}
2123

pkg/harness/exec/exec_types.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@ type executable struct {
4141

4242
// logSink is a custom writer that can be used to forward
4343
// executable output. It does not stop output from being forwarded
44-
// to the os.Stdout.
45-
logSink io.WriteCloser
44+
// to the [os.Stdout].
45+
outputSinks []io.WriteCloser
4646

4747
// hookFunctions ...
4848
hookFunctions map[v1.Lifecycle]v1.HookFunction
4949
}
5050

5151
type Option func(*executable)
52-
53-
type ArgsModifier func([]string) []string
Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,51 @@
11
package ansible
22

33
import (
4+
"os"
5+
"path"
6+
47
console "github.com/pluralsh/console-client-go"
8+
"github.com/samber/lo"
9+
"k8s.io/klog/v2"
510

11+
"github.com/pluralsh/deployment-operator/internal/helpers"
612
v1 "github.com/pluralsh/deployment-operator/pkg/harness/tool/v1"
13+
"github.com/pluralsh/deployment-operator/pkg/log"
714
)
815

16+
// Plan implements [v1.Tool] interface.
917
func (in *Ansible) Plan() (*console.StackStateAttributes, error) {
10-
// TODO implement me
11-
panic("implement me")
18+
output, err := os.ReadFile(in.planFilePath)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
klog.V(log.LogLevelTrace).InfoS("ansible plan file read successfully", "file", in.planFilePath, "output", string(output))
24+
return &console.StackStateAttributes{
25+
Plan: lo.ToPtr(string(output)),
26+
}, nil
1227
}
1328

14-
func (in *Ansible) State() (*console.StackStateAttributes, error) {
15-
// TODO implement me
16-
panic("implement me")
17-
}
29+
// Modifier implements [v1.Tool] interface.
30+
func (in *Ansible) Modifier(stage console.StepStage) v1.Modifier {
31+
globalEnvModifier := NewGlobalEnvModifier(in.workDir)
1832

19-
func (in *Ansible) Output() ([]*console.StackOutputAttributes, error) {
20-
// TODO implement me
21-
panic("implement me")
22-
}
33+
if stage == console.StepStagePlan {
34+
return v1.NewMultiModifier(NewPassthroughModifier(in.planFilePath), globalEnvModifier)
35+
}
2336

24-
func (in *Ansible) Modifier(stage console.StepStage) v1.Modifier {
25-
// TODO implement me
26-
panic("implement me")
37+
return globalEnvModifier
2738
}
2839

29-
func (in *Ansible) ConfigureStateBackend(actor, deployToken string, urls *console.StackRunBaseFragment_StateUrls) error {
30-
// TODO implement me
31-
panic("implement me")
40+
func (in *Ansible) init() *Ansible {
41+
in.planFileName = "ansible.plan"
42+
in.planFilePath = path.Join(in.execDir, in.planFileName)
43+
helpers.EnsureFileOrDie(in.planFilePath)
44+
45+
return in
3246
}
3347

34-
func New(dir string) *Ansible {
35-
return &Ansible{dir: dir}
48+
// New creates an Ansible structure that implements v1.Tool interface.
49+
func New(workDir, execDir string) *Ansible {
50+
return (&Ansible{workDir: workDir, execDir: execDir}).init()
3651
}
Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
package ansible
22

3+
import (
4+
v1 "github.com/pluralsh/deployment-operator/pkg/harness/tool/v1"
5+
)
6+
37
// Ansible implements tool.Tool interface.
48
type Ansible struct {
5-
// dir
6-
dir string
7-
}
9+
v1.DefaultTool
10+
11+
// workDir
12+
workDir string
13+
14+
// execDir
15+
execDir string
16+
17+
// planFileName
18+
planFileName string
819

9-
// Modifier implements tool.Modifier interface.
10-
type Modifier struct {
20+
// planFilePath
21+
planFilePath string
1122
}
Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,39 @@
11
package ansible
22

33
import (
4-
console "github.com/pluralsh/console-client-go"
4+
"fmt"
5+
"io"
6+
"os"
7+
"path"
58

6-
"github.com/pluralsh/deployment-operator/pkg/harness/exec"
9+
"k8s.io/klog/v2"
10+
11+
v1 "github.com/pluralsh/deployment-operator/pkg/harness/tool/v1"
712
)
813

9-
func (in *Modifier) Args(stage console.StepStage) exec.ArgsModifier {
10-
// TODO implement me
11-
panic("implement me")
14+
func (in *PassthroughModifier) WriteCloser() []io.WriteCloser {
15+
f, err := os.OpenFile(in.planFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
16+
if err != nil {
17+
klog.Errorf("failed to open ansible plan file: %v", err)
18+
}
19+
20+
return []io.WriteCloser{f}
21+
}
22+
23+
func NewPassthroughModifier(planFile string) v1.Modifier {
24+
return &PassthroughModifier{planFile: planFile}
25+
}
26+
27+
func (in *GlobalEnvModifier) Env(env []string) []string {
28+
ansibleHome := path.Join(in.workDir, ansibleDir)
29+
ansibleTmp := path.Join(ansibleHome, ansibleTmpDir)
30+
31+
return append(env,
32+
fmt.Sprintf("ANSIBLE_HOME=%s", ansibleHome),
33+
fmt.Sprintf("ANSIBLE_REMOTE_TMP=%s", ansibleTmp),
34+
)
35+
}
36+
37+
func NewGlobalEnvModifier(workDir string) v1.Modifier {
38+
return &GlobalEnvModifier{workDir: workDir}
1239
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package ansible
2+
3+
import (
4+
v1 "github.com/pluralsh/deployment-operator/pkg/harness/tool/v1"
5+
)
6+
7+
// PassthroughModifier implements [v1.PassthroughModifier] interface.
8+
type PassthroughModifier struct {
9+
v1.DefaultModifier
10+
11+
// planFile
12+
planFile string
13+
}
14+
15+
// GlobalEnvModifier implements [v1.EnvModifier] interface.
16+
type GlobalEnvModifier struct {
17+
v1.DefaultModifier
18+
19+
// workDir
20+
workDir string
21+
}
22+
23+
const (
24+
ansibleDir = ".ansible"
25+
ansibleTmpDir = "tmp"
26+
)

0 commit comments

Comments
 (0)