From d4e934fee81946b352b78184e7451df930f2d0d5 Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Wed, 20 Mar 2024 09:15:27 -0700 Subject: [PATCH] initial version --- go.mod | 28 +++++++ go.sum | 82 ++++++++++++++++++++ internal/resource/document.go | 35 +++++++++ internal/resource/document_test.go | 59 ++++++++++++++ internal/resource/file.go | 119 +++++++++++++++++++++++++++++ internal/resource/file_test.go | 91 ++++++++++++++++++++++ internal/resource/os.go | 34 +++++++++ internal/resource/os_test.go | 32 ++++++++ internal/resource/resource.go | 57 ++++++++++++++ internal/resource/resource_test.go | 74 ++++++++++++++++++ internal/resource/types.go | 30 ++++++++ internal/resource/types_test.go | 32 ++++++++ internal/resource/user.go | 102 +++++++++++++++++++++++++ internal/resource/user_test.go | 45 +++++++++++ tests/mocks/container.go | 15 ++++ tests/mocks/resource.go | 14 ++++ 16 files changed, 849 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/resource/document.go create mode 100644 internal/resource/document_test.go create mode 100644 internal/resource/file.go create mode 100644 internal/resource/file_test.go create mode 100644 internal/resource/os.go create mode 100644 internal/resource/os_test.go create mode 100644 internal/resource/resource.go create mode 100644 internal/resource/resource_test.go create mode 100644 internal/resource/types.go create mode 100644 internal/resource/types_test.go create mode 100644 internal/resource/user.go create mode 100644 internal/resource/user_test.go create mode 100644 tests/mocks/container.go create mode 100644 tests/mocks/resource.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d8d075c --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module decl + +go 1.21.1 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/docker v25.0.3+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/sys v0.1.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e511819 --- /dev/null +++ b/go.sum @@ -0,0 +1,82 @@ +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ= +github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/resource/document.go b/internal/resource/document.go new file mode 100644 index 0000000..0a3b660 --- /dev/null +++ b/internal/resource/document.go @@ -0,0 +1,35 @@ +package resource + +import ( +_ "fmt" + "io" + "gopkg.in/yaml.v3" +) + +type Document struct { + Nodes []yaml.Node `yaml:"resources"` + ResourceDecls []Resource `-` +} + +func NewDocument() *Document { + return &Document {} +} + +func (d *Document) Load(r io.Reader) error { + yamlDecoder := yaml.NewDecoder(r) + yamlDecoder.Decode(d) + d.ResourceDecls = make([]Resource, len(d.Nodes)) + for i,node := range(d.Nodes) { + resourceDecl := NewDeclaration() + node.Decode(resourceDecl) + if r,e := ResourceTypes.New(resourceDecl.Type); e == nil { + resourceDecl.Attributes.Decode(r) + d.ResourceDecls[i] = r + } + } + return nil +} + +func (d *Document) Resources() []Resource { + return d.ResourceDecls +} diff --git a/internal/resource/document_test.go b/internal/resource/document_test.go new file mode 100644 index 0000000..95dbbd5 --- /dev/null +++ b/internal/resource/document_test.go @@ -0,0 +1,59 @@ +package resource + +import ( + "os" + "fmt" + "log" + "strings" + "path/filepath" + "testing" + "github.com/stretchr/testify/assert" +) + +func TestNewDocumentLoader(t *testing.T) { + d := NewDocument() + assert.NotEqual(t, nil, d) +} + +func TestDocumentLoader(t *testing.T) { + dir, err := os.MkdirTemp("", "testdocumentloader") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) + + file := filepath.Join(dir, "foo.txt") + + document := fmt.Sprintf(` +--- +resources: +- type: file + attributes: + path: "%s" + owner: "nobody" + group: "nobody" + mode: "0600" + content: |- + test line 1 + test line 2 + state: present +- type: user + attributes: + name: "testuser" + uid: "10022" + gid: "10022" + home: "/home/testuser" + state: present +`, file) + + d := NewDocument() + assert.NotEqual(t, nil, d) + + docReader := strings.NewReader(document) + + e := d.Load(docReader) + assert.Equal(t, nil, e) + + resources := d.Resources() + assert.Equal(t, 2, len(resources)) +} diff --git a/internal/resource/file.go b/internal/resource/file.go new file mode 100644 index 0000000..90b579e --- /dev/null +++ b/internal/resource/file.go @@ -0,0 +1,119 @@ +package resource + +import ( + "fmt" + "os" + "os/user" + "io" + "syscall" + "gopkg.in/yaml.v3" + "strconv" +) + +func init() { + ResourceTypes.Register("file", func() Resource { return NewFile() }) +} + +type File struct { + loader YamlLoader + Path string `yaml:"path"` + Owner string `yaml:"owner"` + Group string `yaml:"group"` + Mode string `yaml:"mode"` + Content string `yaml:"content"` + State string `yaml:"state"` +} + +func NewFile() *File { + return &File{ loader: YamlLoadDecl } +} + +func (f *File) Apply() error { + + switch f.State { + case "absent": + removeErr := os.Remove(f.Path) + if removeErr != nil { + return removeErr + } + case "present": { + uid,uidErr := LookupUID(f.Owner) + if uidErr != nil { + return uidErr + } + + gid,gidErr := LookupGID(f.Group) + if gidErr != nil { + return gidErr + } + + //e := os.Stat(f.path) + //if os.IsNotExist(e) { + createdFile,e := os.Create(f.Path) + if e != nil { + return e + } + defer createdFile.Close() + + if chownErr := createdFile.Chown(uid, gid); chownErr != nil { + return chownErr + } + + mode,modeErr := strconv.ParseInt(f.Mode, 8, 64) + if modeErr != nil { + return modeErr + } + + if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil { + return chmodErr + } + + _,writeErr := createdFile.Write([]byte(f.Content)) + if writeErr != nil { + return writeErr + } + } + } + + return nil +} + +func (f *File) LoadDecl(yamlFileResourceDeclaration string) error { + return f.loader(yamlFileResourceDeclaration, f) +} + +func (f *File) Read() ([]byte, error) { + info, e := os.Stat(f.Path) + + if e != nil { + panic(e) + } + + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + fileUser, userErr := user.LookupId(strconv.Itoa(int(stat.Uid))) + if userErr != nil { //UnknownUserIdError + panic(userErr) + } + fileGroup, groupErr := user.LookupGroupId(strconv.Itoa(int(stat.Gid))) + if groupErr != nil { + panic(groupErr) + } + f.Owner = fileUser.Name + f.Group = fileGroup.Name + } + + f.Mode = fmt.Sprintf("%04o", info.Mode().Perm()) + + file, fileErr := os.Open(f.Path) + if fileErr != nil { + panic(fileErr) + } + + fileContent, ioErr := io.ReadAll(file) + if ioErr != nil { + panic(ioErr) + } + f.Content = string(fileContent) + f.State = "present" + return yaml.Marshal(f) +} diff --git a/internal/resource/file_test.go b/internal/resource/file_test.go new file mode 100644 index 0000000..c399b87 --- /dev/null +++ b/internal/resource/file_test.go @@ -0,0 +1,91 @@ +package resource + +import ( + "fmt" +_ "context" + "testing" +_ "net/http" +_ "net/http/httptest" +_ "net/url" +_ "io" + "os" +_ "log" + "path/filepath" + "github.com/stretchr/testify/assert" +_ "encoding/json" +_ "strings" +) + +func TestNewFileResource(t *testing.T) { + f := NewFile() + assert.NotEqual(t, nil, f) +} + +func TestApplyResourceTransformation(t *testing.T) { + f := NewFile() + assert.NotEqual(t, nil, f) + + //e := f.Apply() + //assert.Equal(t, nil, e) +} + +func TestReadFile(t *testing.T) { + file := filepath.Join(TempDir, "fooread.txt") + + decl := fmt.Sprintf(` + path: "%s" + owner: "nobody" + group: "nobody" + mode: "0600" + content: |- + test line 1 + test line 2 + state: present +`, file) + + testFile := NewFile() + e := testFile.LoadDecl(decl) + assert.Equal(t, nil, e) + testFile.Apply() + + f := NewFile() + assert.NotEqual(t, nil, f) + + f.Path = file + r,e := f.Read() + assert.Equal(t, nil, e) + assert.Equal(t, "nobody", f.Owner) + assert.YAMLEq(t, decl, string(r)) +} + +func TestCreateFile(t *testing.T) { + file := filepath.Join(TempDir, "foo.txt") + + decl := fmt.Sprintf(` + path: "%s" + owner: "nobody" + group: "nobody" + mode: "0600" + content: |- + test line 1 + test line 2 + state: present +`, file) + + f := NewFile() + e := f.LoadDecl(decl) + assert.Equal(t, nil, e) + assert.Equal(t, "nobody", f.Owner) + + applyErr := f.Apply() + assert.Equal(t, nil, applyErr) + assert.FileExists(t, file, nil) + s,e := os.Stat(file) + assert.Equal(t, nil, e) + + assert.Greater(t, s.Size(), int64(0)) + + f.State = "absent" + assert.Equal(t, nil, f.Apply()) + assert.NoFileExists(t, file, nil) +} diff --git a/internal/resource/os.go b/internal/resource/os.go new file mode 100644 index 0000000..c618f26 --- /dev/null +++ b/internal/resource/os.go @@ -0,0 +1,34 @@ +package resource + +import ( + "os/user" + "strconv" +) + +func LookupUID(userName string) (int,error) { + user, userLookupErr := user.Lookup(userName) + if userLookupErr != nil { + return -1,userLookupErr + } + + uid, uidErr := strconv.Atoi(user.Uid) + if uidErr != nil { + return -1,uidErr + } + + return uid,nil +} + +func LookupGID(groupName string) (int,error) { + group, groupLookupErr := user.LookupGroup(groupName) + if groupLookupErr != nil { + return -1,groupLookupErr + } + + gid, gidErr := strconv.Atoi(group.Gid) + if gidErr != nil { + return -1,gidErr + } + + return gid, nil +} diff --git a/internal/resource/os_test.go b/internal/resource/os_test.go new file mode 100644 index 0000000..2638b47 --- /dev/null +++ b/internal/resource/os_test.go @@ -0,0 +1,32 @@ +package resource + +import ( +_ "fmt" +_ "context" + "testing" +_ "net/http" +_ "net/http/httptest" +_ "net/url" +_ "io" + "github.com/stretchr/testify/assert" +_ "encoding/json" +_ "strings" +) + +func TestLookupUID(t *testing.T) { + uid,e := LookupUID("nobody") + + assert.Equal(t, nil, e) + assert.Equal(t, 65534, uid) +} + +func TestLookupGID(t *testing.T) { + gid,e := LookupGID("nobody") + + assert.Equal(t, nil, e) + assert.Equal(t, 65534, gid) +} + +func TestExecCommand(t *testing.T) { + +} diff --git a/internal/resource/resource.go b/internal/resource/resource.go new file mode 100644 index 0000000..bb7bc40 --- /dev/null +++ b/internal/resource/resource.go @@ -0,0 +1,57 @@ +package resource + +import ( + "gopkg.in/yaml.v3" +) + +type Declaration struct { + Type string `yaml:"type"` + Attributes yaml.Node `yaml:"attributes"` +} + +type Resource interface { + ResourceLoader + StateTransformer +} + +type ResourceLoader interface { + LoadDecl(string) error +} + +type StateTransformer interface { + Apply() error +} + +type YamlLoader func(string, any) error + +func YamlLoadDecl(yamlFileResourceDeclaration string, resource any) error { + if err := yaml.Unmarshal([]byte(yamlFileResourceDeclaration), resource); err != nil { + return err + } + return nil +} + +type ResourceCreator interface { + Create() error +} + +type ResourceReader interface { + Read() ([]byte, error) +} + +type ResourceUpdater interface { + Update() error +} + +type ResourceDeleter interface { + Delete() error +} + + +func NewDeclaration() *Declaration { + return &Declaration{} +} + +func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error { + return YamlLoadDecl(yamlResourceDeclaration, d) +} diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go new file mode 100644 index 0000000..1782d49 --- /dev/null +++ b/internal/resource/resource_test.go @@ -0,0 +1,74 @@ +package resource + +import ( + "os" + "path/filepath" + "fmt" + "log" + "testing" + "github.com/stretchr/testify/assert" +) + +var TempDir string + +func TestMain(m *testing.M) { + var err error + TempDir, err = os.MkdirTemp("", "testresourcefile") + if err != nil || TempDir == "" { + log.Fatal(err) + } + defer os.RemoveAll(TempDir) + + rc := m.Run() + + os.Exit(rc) +} + +func TestYamlLoadDecl(t *testing.T) { + + file := filepath.Join(TempDir, "fooread.txt") + + resourceAttributes := make(map[string]any) + decl := fmt.Sprintf(` + path: "%s" + owner: "nobody" + group: "nobody" + mode: "0600" + content: |- + test line 1 + test line 2 +`, file) + + e := YamlLoadDecl(decl, &resourceAttributes) + assert.Equal(t, nil, e) + + assert.Equal(t, "nobody", resourceAttributes["group"]) +} + +func TestNewResourceDeclaration(t *testing.T) { + resourceDeclaration := NewDeclaration() + assert.NotEqual(t, nil, resourceDeclaration) +} + +func TestNewResourceDeclarationType(t *testing.T) { + file := filepath.Join(TempDir, "fooread.txt") + + decl := fmt.Sprintf(` + type: file + attributes: + path: "%s" + owner: "nobody" + group: "nobody" + mode: "0600" + content: |- + test line 1 + test line 2 +`, file) + + resourceDeclaration := NewDeclaration() + assert.NotEqual(t, nil, resourceDeclaration) + + resourceDeclaration.LoadDecl(decl) + assert.Equal(t, "file", resourceDeclaration.Type) + assert.NotEqual(t, nil, resourceDeclaration.Attributes) +} diff --git a/internal/resource/types.go b/internal/resource/types.go new file mode 100644 index 0000000..f17c825 --- /dev/null +++ b/internal/resource/types.go @@ -0,0 +1,30 @@ +package resource + +import ( + "fmt" +) + +var ( + ResourceTypes *Types = NewTypes() +) + +type TypeFactory func() Resource + +type Types struct { + registry map[string]TypeFactory +} + +func NewTypes() *Types { + return &Types{ registry: make(map[string]TypeFactory) } +} + +func (t *Types) Register(name string, factory TypeFactory) { + t.registry[name] = factory +} + +func (t *Types) New(name string) (Resource, error) { + if r,ok := t.registry[name]; ok { + return r(), nil + } + return nil, fmt.Errorf("Unknown type: %s", name) +} diff --git a/internal/resource/types_test.go b/internal/resource/types_test.go new file mode 100644 index 0000000..8b7d89f --- /dev/null +++ b/internal/resource/types_test.go @@ -0,0 +1,32 @@ +package resource + +import ( + "testing" + "github.com/stretchr/testify/assert" + "decl/tests/mocks" +) + +func TestNewResourceTypes(t *testing.T) { + resourceTypes := NewTypes() + assert.NotEqual(t, nil, resourceTypes) +} + +func TestNewResourceTypesRegister(t *testing.T) { + m := &mocks.MockResource { + InjectLoadDecl: func(string) error { return nil }, + InjectApply: func() error { return nil }, + } + + resourceTypes := NewTypes() + assert.NotEqual(t, nil, resourceTypes) + + resourceTypes.Register("foo", func() Resource { return m }) + + r,e := resourceTypes.New("foo") + assert.Equal(t, nil, e) + assert.Equal(t, m, r) +} + +func TestResourceTypesLoadResource(t *testing.T) { + +} diff --git a/internal/resource/user.go b/internal/resource/user.go new file mode 100644 index 0000000..d08704f --- /dev/null +++ b/internal/resource/user.go @@ -0,0 +1,102 @@ +package resource + +import ( + "fmt" +_ "os" +_ "gopkg.in/yaml.v3" + "os/exec" + "strings" + "log" +) + +type User struct { + loader YamlLoader + Name string `yaml:"name"` + UID int `yaml:"uid"` + Group string `yaml:"group"` + Groups []string `yaml:"groups",omitempty` + Gecos string `yaml:"gecos"` + Home string `yaml:"home"` + CreateHome bool `yaml:"createhome"omitempty` + Shell string `yaml:"shell"` + + State string `yaml:"state"` +} + +func NewUser() *User { + return &User{ loader: YamlLoadDecl } +} + +func (u *User) Apply() error { + switch u.State { + case "present": + _, NoUserExists := LookupUID(u.Name) + if NoUserExists != nil { + var userCommandName string = "useradd" + args := make([]string, 0, 7) + if u.UID >= 0 { + args = append(args, "-u", fmt.Sprintf("%d", u.UID)) + } + + if _,pathErr := exec.LookPath("useradd"); pathErr != nil { + if _,addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil { + userCommandName = "adduser" + u.AddUserCommand(&args) + } + } else { + u.UserAddCommand(&args) + } + args = append(args, u.Name) + cmd := exec.Command(userCommandName, args...) + cmdOutput, cmdErr := cmd.CombinedOutput() + log.Printf("%s\n", cmdOutput) + return cmdErr + } + case "absent": + var userDelCommandName string = "userdel" + args := make([]string, 0, 7) + + if _,pathErr := exec.LookPath("userdel"); pathErr != nil { + if _,delUserPathErr := exec.LookPath("deluser"); delUserPathErr == nil { + userDelCommandName = "deluser" + } + } + args = append(args, u.Name) + cmd := exec.Command(userDelCommandName, args...) + cmdOutput, cmdErr := cmd.CombinedOutput() + log.Printf("%s\n", cmdOutput) + return cmdErr + } + return nil +} + +func (u *User) LoadDecl(yamlFileResourceDeclaration string) error { + return u.loader(yamlFileResourceDeclaration, u) +} + +func (u *User) AddUserCommand(args *[]string) error { + *args = append(*args, "-D") + if u.Group != "" { + *args = append(*args, "-G", u.Group) + } + if u.Home != "" { + *args = append(*args, "-h", u.Home) + } + return nil +} + +func (u *User) UserAddCommand(args *[]string) error { + if u.Group != "" { + *args = append(*args, "-g", u.Group) + } + if len(u.Groups) > 0 { + *args = append(*args, "-G", strings.Join(u.Groups, ",")) + } + if u.Home != "" { + *args = append(*args, "-d", u.Home) + } + if u.CreateHome { + *args = append(*args, "-m") + } + return nil +} diff --git a/internal/resource/user_test.go b/internal/resource/user_test.go new file mode 100644 index 0000000..35073e7 --- /dev/null +++ b/internal/resource/user_test.go @@ -0,0 +1,45 @@ +package resource + +import ( +_ "fmt" +_ "context" + "testing" +_ "net/http" +_ "net/http/httptest" +_ "net/url" +_ "io" +_ "os" + "github.com/stretchr/testify/assert" +_ "encoding/json" +_ "strings" +) + +func TestNewUserResource(t *testing.T) { + u := NewUser() + assert.NotEqual(t, nil, u) +} + +func TestCreateUser(t *testing.T) { + decl := ` + name: "testuser" + uid: 12001 + gid: 12001 + home: "/home/testuser" + state: present +` + u := NewUser() + e := u.LoadDecl(decl) + assert.Equal(t, nil, e) + assert.Equal(t, "testuser", u.Name) + + applyErr := u.Apply() + assert.Equal(t, nil, applyErr) + uid, uidErr := LookupUID(u.Name) + assert.Equal(t, nil, uidErr) + assert.Equal(t, 12001, uid) + + u.State = "absent" + + applyDeleteErr := u.Apply() + assert.Equal(t, nil, applyDeleteErr) +} diff --git a/tests/mocks/container.go b/tests/mocks/container.go new file mode 100644 index 0000000..3eb23a9 --- /dev/null +++ b/tests/mocks/container.go @@ -0,0 +1,15 @@ +package mocks + +/* + +import ( + "net/http/httptest" +) + +func newMockClient(doer func(*http.Request) (*http.Response, error)) *http.Client { + return &http.Client{ + Transport: transportEnsureBody(transportFunc(doer)), + } +} + +*/ diff --git a/tests/mocks/resource.go b/tests/mocks/resource.go new file mode 100644 index 0000000..099842b --- /dev/null +++ b/tests/mocks/resource.go @@ -0,0 +1,14 @@ +package mocks + +type MockResource struct { + InjectLoadDecl func(string) error + InjectApply func() error +} + +func (m *MockResource) LoadDecl(yamlResourceDeclaration string) error { + return m.InjectLoadDecl(yamlResourceDeclaration) +} + +func (m *MockResource) Apply() error { + return m.InjectApply() +}