feat(systemd): add resource plugin for systemd unit files by ScMofeoluwa · Pull Request #8970 · updatecli/updatecli · GitHub
Skip to content
Merged
1 change: 1 addition & 0 deletions go.mod
6 changes: 6 additions & 0 deletions pkg/core/pipeline/resource/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/updatecli/updatecli/pkg/plugins/resources/shell"
stashBranch "github.com/updatecli/updatecli/pkg/plugins/resources/stash/branch"
stashTag "github.com/updatecli/updatecli/pkg/plugins/resources/stash/tag"
"github.com/updatecli/updatecli/pkg/plugins/resources/systemd"
"github.com/updatecli/updatecli/pkg/plugins/resources/temurin"
terraformLock "github.com/updatecli/updatecli/pkg/plugins/resources/terraform/lock"
terraformProvider "github.com/updatecli/updatecli/pkg/plugins/resources/terraform/provider"
Expand Down Expand Up @@ -225,6 +226,10 @@ func New(rs ResourceConfig) (resource Resource, err error) {

return stashTag.New(rs.Spec)

case "systemd":

return systemd.New(rs.Spec)

case "terraform/file":

return hcl.New(rs.Spec)
Expand Down Expand Up @@ -318,6 +323,7 @@ func GetResourceMapping() map[string]interface{} {
"shell": &shell.Spec{},
"stash/branch": &stashBranch.Spec{},
"stash/tag": &stashTag.Spec{},
"systemd": &systemd.Spec{},
"temurin": &temurin.Spec{},
"terraform/file": &hcl.Spec{},
"terraform/lock": &terraformLock.Spec{},
Expand Down
77 changes: 77 additions & 0 deletions pkg/plugins/resources/systemd/condition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package systemd

import (
"context"
"fmt"
"path/filepath"

"github.com/sirupsen/logrus"
"github.com/updatecli/updatecli/pkg/core/pipeline/scm"
)

func (s *Systemd) Condition(_ context.Context, source string, scm scm.ScmHandler) (pass bool, message string, err error) {
expected := s.spec.Value
if expected == "" {
// Override the default value with the source output when the `spec.value` is not set.
expected = source
}

rootDir := ""
if scm != nil {
rootDir = scm.GetDirectory()
}

filePath := s.spec.File
if !filepath.IsAbs(filePath) {
filePath = filepath.Join(rootDir, filePath)
}

_, matchingOpts, err := s.readOptions(filePath)
if err != nil {
return false, "", err
}

if matchingOpts == nil {
return false, fmt.Sprintf("option %q not found in section %q in the systemd unit file %q",
s.spec.Option, s.spec.Section, filePath), nil
}

optIndex := 0
if s.spec.Index != nil {
optIndex = *s.spec.Index
}

switch s.spec.Index {
case nil:
totalNotMatchingOpts := 0
for i, opt := range matchingOpts {
if opt.Section == s.spec.Section && opt.Name == s.spec.Option {
if expected != opt.Value {
logrus.Debugf("option %d %q value %q doesn't match expected value %q",
i, opt.Name, opt.Value, expected)
totalNotMatchingOpts++
}
}
}

if totalNotMatchingOpts > 0 {
return false, fmt.Sprintf("%d option(s) %q value(s) doesn't match expected value %q",
totalNotMatchingOpts, s.spec.Option, expected), nil
}

default:

if optIndex >= len(matchingOpts) {
return false, fmt.Sprintf("option %q with index %d not found in section %q in the systemd unit file %q",
s.spec.Option, optIndex, s.spec.Section, filePath), nil
}

if matchingOpts[optIndex].Value != expected {
return false, fmt.Sprintf("option %q value %q doesn't match expected value %q",
matchingOpts[optIndex].Name, matchingOpts[optIndex].Value, expected), nil
}
}

return true, fmt.Sprintf("option %q matching value %q found in section %q",
matchingOpts[optIndex].Name, matchingOpts[optIndex].Value, matchingOpts[optIndex].Section), nil
}
160 changes: 160 additions & 0 deletions pkg/plugins/resources/systemd/condition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package systemd

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/updatecli/updatecli/pkg/core/text"
)

func TestCondition(t *testing.T) {
tests := []struct {
name string
spec Spec
source string
mockedContents map[string]string
shouldPass bool
wantErr bool
}{
{
name: "Value matches from source",
source: "nginx:1.25",
spec: Spec{
File: "test.container",
Section: "Container",
Option: "Image",
},
mockedContents: map[string]string{
"test.container": "[Unit]\nDescription=test\n\n[Container]\nImage=nginx:1.25\n",
},
shouldPass: true,
},
{
name: "Value matches from spec.Value",
source: "",
spec: Spec{
File: "test.container",
Section: "Container",
Option: "Image",
Value: "nginx:1.25",
},
mockedContents: map[string]string{
"test.container": "[Unit]\nDescription=test\n\n[Container]\nImage=nginx:1.25\n",
},
shouldPass: true,
},
{
name: "Value from spec takes precedence over source",
source: "nginx:1.26",
spec: Spec{
File: "test.container",
Section: "Container",
Option: "Image",
Value: "nginx:1.25",
},
mockedContents: map[string]string{
"test.container": "[Unit]\nDescription=test\n\n[Container]\nImage=nginx:1.25\n",
},
shouldPass: true,
},
{
name: "Value matches repeated option index",
source: "/etc/wg-easy:/etc/wireguard:rw",
spec: Spec{
File: "test.container",
Section: "Container",
Option: "Volume",
Index: intPtr(1),
},
mockedContents: map[string]string{
"test.container": "[Container]\nVolume=/lib/modules:/lib/modules:ro\nVolume=/etc/wg-easy:/etc/wireguard:rw\n",
},
shouldPass: true,
},
{
name: "Value doesn't matches all repeated option index",
source: "/etc/wg-easy:/etc/wireguard:rw",
spec: Spec{
File: "test.container",
Section: "Container",
Option: "Volume",
},
mockedContents: map[string]string{
"test.container": "[Container]\nVolume=/lib/modules:/lib/modules:ro\nVolume=/etc/wg-easy:/etc/wireguard:rw\n",
},
shouldPass: false,
},
{
name: "Value matches all repeated option index",
source: "/lib/modules:/lib/modules:ro",
spec: Spec{
File: "test.container",
Section: "Container",
Option: "Volume",
},
mockedContents: map[string]string{
"test.container": "[Container]\nVolume=/lib/modules:/lib/modules:ro\nVolume=/lib/modules:/lib/modules:ro\n",
},
shouldPass: true,
},
{
name: "Value does not match",
source: "nginx:1.26",
spec: Spec{
File: "test.container",
Section: "Container",
Option: "Image",
},
mockedContents: map[string]string{
"test.container": "[Unit]\nDescription=test\n\n[Container]\nImage=nginx:1.25\n",
},
shouldPass: false,
},
{
name: "File does not exist",
source: "nginx:1.25",
spec: Spec{
File: "nonexistent.container",
Section: "Container",
Option: "Image",
},
mockedContents: map[string]string{},
wantErr: true,
},
{
name: "Option not found in file",
source: "nginx:1.25",
spec: Spec{
File: "test.container",
Section: "Container",
Option: "NonExistent",
},
mockedContents: map[string]string{
"test.container": "[Unit]\nDescription=test\n\n[Container]\nImage=nginx:1.25\n",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockedText := text.MockTextRetriever{
Contents: tt.mockedContents,
}
s := &Systemd{
spec: tt.spec,
contentRetriever: &mockedText,
}

pass, _, err := s.Condition(context.Background(), tt.source, nil)
if tt.wantErr {
assert.Error(t, err)
return
}

require.NoError(t, err)
assert.Equal(t, tt.shouldPass, pass)
})
}
}
98 changes: 98 additions & 0 deletions pkg/plugins/resources/systemd/main.go
Loading
Loading