From dfd5a8964d334ba4ecc371ff8f552fc352f4271d Mon Sep 17 00:00:00 2001 From: Allison Richardet Date: Mon, 29 Aug 2016 20:31:47 +0000 Subject: [PATCH] Add user group support --- load/resource.go | 1 + resource/group/group.go | 166 ++++++++++++ resource/group/group_linux.go | 58 ++++ resource/group/group_test.go | 466 ++++++++++++++++++++++++++++++++ resource/group/preparer.go | 86 ++++++ resource/group/preparer_test.go | 105 +++++++ samples/group.hcl | 5 + 7 files changed, 887 insertions(+) create mode 100644 resource/group/group.go create mode 100644 resource/group/group_linux.go create mode 100644 resource/group/group_test.go create mode 100644 resource/group/preparer.go create mode 100644 resource/group/preparer_test.go create mode 100644 samples/group.hcl diff --git a/load/resource.go b/load/resource.go index 8b1dc7bc3..4579c68df 100644 --- a/load/resource.go +++ b/load/resource.go @@ -29,6 +29,7 @@ import ( _ "github.com/asteris-llc/converge/resource/docker/image" _ "github.com/asteris-llc/converge/resource/file/content" _ "github.com/asteris-llc/converge/resource/file/mode" + _ "github.com/asteris-llc/converge/resource/group" _ "github.com/asteris-llc/converge/resource/module" _ "github.com/asteris-llc/converge/resource/param" _ "github.com/asteris-llc/converge/resource/shell" diff --git a/resource/group/group.go b/resource/group/group.go new file mode 100644 index 000000000..e18c5ab7d --- /dev/null +++ b/resource/group/group.go @@ -0,0 +1,166 @@ +// Copyright © 2016 Asteris, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group + +import ( + "fmt" + "os/user" + + "github.com/asteris-llc/converge/resource" +) + +// State type for Group +type State string + +const ( + // StatePresent indicates the group should be present + StatePresent State = "present" + + // StateAbsent indicates the group should be absent + StateAbsent State = "absent" +) + +// Group manages user groups +type Group struct { + GID string + Name string + State State + system SystemUtils +} + +// SystemUtils provides system utilities for group +type SystemUtils interface { + AddGroup(string, string) error + DelGroup(string) error + LookupGroup(string) (*user.Group, error) + LookupGroupID(string) (*user.Group, error) +} + +// NewGroup constructs and returns a new Group +func NewGroup(system SystemUtils) *Group { + return &Group{ + system: system, + } +} + +// Check if a user group exists +func (g *Group) Check(resource.Renderer) (resource.TaskStatus, error) { + // lookup the group by name and lookup the group by gid + // the lookup returns an error if the group is not found + groupByName, nameErr := g.system.LookupGroup(g.Name) + groupByGid, gidErr := g.system.LookupGroupID(g.GID) + + status := &resource.Status{} + + switch g.State { + case StatePresent: + _, nameNotFound := nameErr.(user.UnknownGroupError) + _, gidNotFound := gidErr.(user.UnknownGroupIdError) + + switch { + case nameNotFound && gidNotFound: + status.WarningLevel = resource.StatusWillChange + status.Output = append(status.Output, "group name and gid do not exist") + status.AddDifference("group", string(StateAbsent), fmt.Sprintf("group %s with gid %s", g.Name, g.GID), "") + status.WillChange = true + case nameNotFound: + status.WarningLevel = resource.StatusFatal + status.Output = append(status.Output, fmt.Sprintf("group gid %s already exists", g.GID)) + case gidNotFound: + status.WarningLevel = resource.StatusFatal + status.Output = append(status.Output, fmt.Sprintf("group %s already exists", g.Name)) + case groupByName.Name != groupByGid.Name || groupByName.Gid != groupByGid.Gid: + status.WarningLevel = resource.StatusFatal + status.Output = append(status.Output, fmt.Sprintf("group %s and gid %s belong to different groups", g.Name, g.GID)) + case groupByName != nil && groupByGid != nil && *groupByName == *groupByGid: + status.WarningLevel = resource.StatusNoChange + } + case StateAbsent: + _, nameNotFound := nameErr.(user.UnknownGroupError) + _, gidNotFound := gidErr.(user.UnknownGroupIdError) + + switch { + case nameNotFound && gidNotFound: + status.WarningLevel = resource.StatusNoChange + status.Output = append(status.Output, "group name and gid do not exist") + case nameNotFound: + status.WarningLevel = resource.StatusFatal + status.Output = append(status.Output, fmt.Sprintf("group %s does not exist", g.Name)) + case gidNotFound: + status.WarningLevel = resource.StatusFatal + status.Output = append(status.Output, fmt.Sprintf("group gid %s does not exist", g.GID)) + case groupByName.Name != groupByGid.Name || groupByName.Gid != groupByGid.Gid: + status.WarningLevel = resource.StatusFatal + status.Output = append(status.Output, fmt.Sprintf("group %s and gid %s belong to different groups", g.Name, g.GID)) + case groupByName != nil && groupByGid != nil && *groupByName == *groupByGid: + status.WarningLevel = resource.StatusWillChange + status.WillChange = true + status.AddDifference("group", fmt.Sprintf("group %s with gid %s", g.Name, g.GID), string(StateAbsent), "") + } + default: + return nil, fmt.Errorf("group: unrecognized state %v", g.State) + } + + return status, nil +} + +// Apply changes for group +func (g *Group) Apply(resource.Renderer) (resource.TaskStatus, error) { + // lookup the group by name and lookup the group by gid + // the lookup returns an error if the group is not found + groupByName, nameErr := g.system.LookupGroup(g.Name) + groupByGid, gidErr := g.system.LookupGroupID(g.GID) + + status := &resource.Status{} + + switch g.State { + case StatePresent: + _, nameNotFound := nameErr.(user.UnknownGroupError) + _, gidNotFound := gidErr.(user.UnknownGroupIdError) + + switch { + case nameNotFound && gidNotFound: + err := g.system.AddGroup(g.Name, g.GID) + if err != nil { + status.WarningLevel = resource.StatusFatal + status.Output = append(status.Output, fmt.Sprintf("error adding group %s with gid %s", g.Name, g.GID)) + return status, err + } + status.Output = append(status.Output, fmt.Sprintf("added group %s with gid %s", g.Name, g.GID)) + default: + status.WarningLevel = resource.StatusFatal + return status, fmt.Errorf("will not attempt add: group %s with gid %s", g.Name, g.GID) + } + case StateAbsent: + switch { + case groupByName != nil && groupByGid != nil && *groupByName == *groupByGid: + err := g.system.DelGroup(g.Name) + if err != nil { + status.WarningLevel = resource.StatusFatal + status.Output = append(status.Output, fmt.Sprintf("error deleting group %s with gid %s", g.Name, g.GID)) + return status, err + } + status.Output = append(status.Output, fmt.Sprintf("deleted group %s with gid %s", g.Name, g.GID)) + default: + status.WarningLevel = resource.StatusFatal + return status, fmt.Errorf("will not attempt delete: group %s with gid %s", g.Name, g.GID) + } + default: + status.WarningLevel = resource.StatusFatal + return status, fmt.Errorf("group: unrecognized state %s", g.State) + } + + return status, nil +} diff --git a/resource/group/group_linux.go b/resource/group/group_linux.go new file mode 100644 index 000000000..ef0ce8cb9 --- /dev/null +++ b/resource/group/group_linux.go @@ -0,0 +1,58 @@ +// Copyright © 2016 Asteris, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build linux + +package group + +import ( + "fmt" + "os/exec" + "os/user" +) + +// System implements SystemUtils +type System struct{} + +// AddGroup adds a group +func (s *System) AddGroup(groupName, groupID string) error { + cmd := exec.Command("groupadd", groupName, "-g", groupID) + err := cmd.Run() + if err != nil { + return fmt.Errorf("groupadd: %s", err) + } + return nil +} + +// DelGroup deletes a group +func (s *System) DelGroup(groupName string) error { + cmd := exec.Command("groupdel", groupName) + err := cmd.Run() + if err != nil { + return fmt.Errorf("groupdel: %s", err) + } + return nil +} + +// LookupGroup looks up a group by name +// If the group cannot be found an error is returned +func (s *System) LookupGroup(groupName string) (*user.Group, error) { + return user.LookupGroup(groupName) +} + +// LookupGroupID looks up a group by gid +// If the group cannot be found an error is returned +func (s *System) LookupGroupID(groupID string) (*user.Group, error) { + return user.LookupGroupId(groupID) +} diff --git a/resource/group/group_test.go b/resource/group/group_test.go new file mode 100644 index 000000000..d17e3252e --- /dev/null +++ b/resource/group/group_test.go @@ -0,0 +1,466 @@ +// Copyright © 2016 Asteris, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group_test + +import ( + "fmt" + "math" + "os/user" + "strconv" + "strings" + "testing" + + "github.com/asteris-llc/converge/helpers/fakerenderer" + "github.com/asteris-llc/converge/resource" + "github.com/asteris-llc/converge/resource/group" + "github.com/fgrid/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var ( + currUser *user.User + currGid string + currGroup *user.Group + currName string + userErr error + groupErr error + tempName []string + fakeName string + fakeGid string + gidErr error +) + +const ( + // Valid GID range varies based on system + // At a minimum, 0-32676 is valid + GID_MIN = 0 + GID_MAX = math.MaxInt16 +) + +func init() { + currUser, userErr = user.Current() + if userErr != nil { + panic(userErr) + } + + currGid = currUser.Gid + currGroup, groupErr = user.LookupGroupId(currGid) + if groupErr != nil { + panic(groupErr) + } + + fakeGid, gidErr = setFakeGid() + if gidErr != nil { + panic(gidErr) + } + + currName = currGroup.Name + tempName = strings.Split(uuid.NewV4().String(), "-") + fakeName = strings.Join(tempName[0:], "") +} + +func TestGroupInterface(t *testing.T) { + t.Parallel() + + assert.Implements(t, (*resource.Task)(nil), new(group.Group)) +} + +func TestCheckFoundGidFoundNameStatePresent(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = currGid + g.Name = currName + g.State = group.StatePresent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusNoChange, status.StatusCode()) + assert.False(t, status.HasChanges()) +} + +func TestCheckFoundGidFoundNameStateAbsent(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = currGid + g.Name = currName + g.State = group.StateAbsent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusWillChange, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("group %s with gid %s", g.Name, g.GID), status.Diffs()["group"].Original()) + assert.Equal(t, string(group.StateAbsent), status.Diffs()["group"].Current()) + assert.True(t, status.HasChanges()) +} + +func TestCheckFoundGidNotNameStatePresent(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = currGid + g.Name = fakeName + g.State = group.StatePresent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("group gid %s already exists", g.GID), status.Messages()[0]) + assert.False(t, status.HasChanges()) +} + +func TestCheckFoundGidNotNameStateAbsent(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = currGid + g.Name = fakeName + g.State = group.StateAbsent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("group %s does not exist", g.Name), status.Messages()[0]) + assert.False(t, status.HasChanges()) +} + +func TestCheckFoundNameNotGidStatePresent(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = fakeGid + g.Name = currName + g.State = group.StatePresent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("group %s already exists", g.Name), status.Messages()[0]) + assert.False(t, status.HasChanges()) +} + +func TestCheckFoundNameNotGidStateAbsent(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = fakeGid + g.Name = currName + g.State = group.StateAbsent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("group gid %s does not exist", g.GID), status.Messages()[0]) + assert.False(t, status.HasChanges()) +} + +func TestCheckNameAndGidMismatchStatePresent(t *testing.T) { + t.Parallel() + + gid, err := setGid() + if err != nil { + panic(err) + } + g := group.NewGroup(new(group.System)) + g.GID = gid + g.Name = currName + g.State = group.StatePresent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("group %s and gid %s belong to different groups", g.Name, g.GID), status.Messages()[0]) + assert.False(t, status.HasChanges()) +} + +func TestCheckNameAndGidMismatchStateAbsent(t *testing.T) { + t.Parallel() + + gid, err := setGid() + if err != nil { + panic(err) + } + g := group.NewGroup(new(group.System)) + g.GID = gid + g.Name = currName + g.State = group.StateAbsent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("group %s and gid %s belong to different groups", g.Name, g.GID), status.Messages()[0]) + assert.False(t, status.HasChanges()) +} + +func TestCheckNameAndGidNotFoundStatePresent(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = fakeGid + g.Name = fakeName + g.State = group.StatePresent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusWillChange, status.StatusCode()) + assert.Equal(t, "group name and gid do not exist", status.Messages()[0]) + assert.Equal(t, string(group.StateAbsent), status.Diffs()["group"].Original()) + assert.Equal(t, fmt.Sprintf("group %s with gid %s", g.Name, g.GID), status.Diffs()["group"].Current()) + assert.True(t, status.HasChanges()) +} + +func TestCheckNameAndGidNotFoundStateAbsent(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = fakeGid + g.Name = fakeName + g.State = group.StateAbsent + status, err := g.Check(fakerenderer.New()) + + assert.NoError(t, err) + assert.Equal(t, resource.StatusNoChange, status.StatusCode()) + assert.Equal(t, "group name and gid do not exist", status.Messages()[0]) + assert.False(t, status.HasChanges()) +} + +func TestCheckStateUnknown(t *testing.T) { + t.Parallel() + + g := group.NewGroup(new(group.System)) + g.GID = fakeGid + g.Name = fakeName + g.State = "test" + _, err := g.Check(fakerenderer.New()) + + assert.EqualError(t, err, fmt.Sprintf("group: unrecognized state %s", g.State)) +} + +func TestApplyAddGroup(t *testing.T) { + t.Parallel() + + grp := &user.Group{ + Name: fakeName, + Gid: fakeGid, + } + m := &MockSystem{} + g := group.NewGroup(m) + g.GID = grp.Gid + g.Name = grp.Name + g.State = group.StatePresent + + m.On("LookupGroup", g.Name).Return(grp, user.UnknownGroupError("")) + m.On("LookupGroupID", g.GID).Return(grp, user.UnknownGroupIdError("")) + m.On("AddGroup", g.Name, g.GID).Return(nil) + status, err := g.Apply(fakerenderer.New()) + + m.AssertCalled(t, "AddGroup", g.Name, g.GID) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf("added group %s with gid %s", g.Name, g.GID), status.Messages()[0]) +} + +func TestApplyDeleteGroup(t *testing.T) { + t.Parallel() + + grp := &user.Group{ + Name: fakeName, + Gid: fakeGid, + } + m := &MockSystem{} + g := group.NewGroup(m) + g.GID = grp.Gid + g.Name = grp.Name + g.State = group.StateAbsent + + m.On("LookupGroup", g.Name).Return(grp, nil) + m.On("LookupGroupID", g.GID).Return(grp, nil) + m.On("DelGroup", g.Name).Return(nil) + status, err := g.Apply(fakerenderer.New()) + + m.AssertCalled(t, "DelGroup", g.Name) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf("deleted group %s with gid %s", g.Name, g.GID), status.Messages()[0]) +} + +func TestApplyAddGroupErrorAdding(t *testing.T) { + t.Parallel() + + grp := &user.Group{ + Name: fakeName, + Gid: fakeGid, + } + m := &MockSystem{} + g := group.NewGroup(m) + g.GID = grp.Gid + g.Name = grp.Name + g.State = group.StatePresent + + m.On("LookupGroup", g.Name).Return(grp, user.UnknownGroupError("")) + m.On("LookupGroupID", g.GID).Return(grp, user.UnknownGroupIdError("")) + m.On("AddGroup", g.Name, g.GID).Return(fmt.Errorf("")) + status, err := g.Apply(fakerenderer.New()) + + m.AssertCalled(t, "AddGroup", g.Name, g.GID) + assert.EqualError(t, err, fmt.Sprintf("")) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("error adding group %s with gid %s", g.Name, g.GID), status.Messages()[0]) +} + +func TestApplyAddGroupNotAdded(t *testing.T) { + t.Parallel() + + grp := &user.Group{ + Name: fakeName, + Gid: fakeGid, + } + m := &MockSystem{} + g := group.NewGroup(m) + g.GID = grp.Gid + g.Name = grp.Name + g.State = group.StatePresent + + m.On("LookupGroup", g.Name).Return(grp, nil) + m.On("LookupGroupID", g.GID).Return(grp, nil) + m.On("AddGroup", g.Name, g.GID).Return(nil) + status, err := g.Apply(fakerenderer.New()) + + m.AssertNotCalled(t, "AddGroup", g.Name, g.GID) + assert.EqualError(t, err, fmt.Sprintf("will not attempt add: group %s with gid %s", g.Name, g.GID)) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) +} + +func TestApplyDeleteGroupErrorDeleting(t *testing.T) { + t.Parallel() + + grp := &user.Group{ + Name: fakeName, + Gid: fakeGid, + } + m := &MockSystem{} + g := group.NewGroup(m) + g.GID = grp.Gid + g.Name = grp.Name + g.State = group.StateAbsent + + m.On("LookupGroup", g.Name).Return(grp, nil) + m.On("LookupGroupID", g.GID).Return(grp, nil) + m.On("DelGroup", g.Name).Return(fmt.Errorf("")) + status, err := g.Apply(fakerenderer.New()) + + m.AssertCalled(t, "DelGroup", g.Name) + assert.EqualError(t, err, fmt.Sprintf("")) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("error deleting group %s with gid %s", g.Name, g.GID), status.Messages()[0]) +} + +func TestApplyDeleteGroupNotDeleted(t *testing.T) { + t.Parallel() + + grp1 := &user.Group{ + Name: fakeName, + Gid: fakeGid, + } + grp2 := &user.Group{ + Name: currName, + Gid: currGid, + } + m := &MockSystem{} + g := group.NewGroup(m) + g.GID = grp2.Gid + g.Name = grp1.Name + g.State = group.StateAbsent + + m.On("LookupGroup", g.Name).Return(grp1, nil) + m.On("LookupGroupID", g.GID).Return(grp2, nil) + m.On("DelGroup", g.Name).Return(nil) + status, err := g.Apply(fakerenderer.New()) + + m.AssertNotCalled(t, "DelGroup", g.Name) + assert.EqualError(t, err, fmt.Sprintf("will not attempt delete: group %s with gid %s", g.Name, g.GID)) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) +} + +func TestApplyStateUnknown(t *testing.T) { + t.Parallel() + + grp := &user.Group{ + Name: fakeName, + Gid: fakeGid, + } + m := &MockSystem{} + g := group.NewGroup(m) + g.GID = grp.Gid + g.Name = grp.Name + g.State = "test" + + m.On("LookupGroup", g.Name).Return(grp, nil) + m.On("LookupGroupID", g.GID).Return(grp, nil) + _, err := g.Apply(fakerenderer.New()) + + m.AssertNotCalled(t, "AddGroup", g.Name, g.GID) + m.AssertNotCalled(t, "DelGroup", g.Name) + assert.EqualError(t, err, fmt.Sprintf("group: unrecognized state %s", g.State)) +} + +// setGid is used for TestCheckNameAndGidMismatch. We need a gid that exists, +// but is not a match for the current user group name (currName). +func setGid() (string, error) { + for i := 0; i <= GID_MAX; i++ { + gid := strconv.Itoa(i) + group, err := user.LookupGroupId(gid) + if err == nil && group.Name != currName { + return gid, nil + } + } + return "", fmt.Errorf("setGid: could not set gid") +} + +// setFakeGid is used to set a gid that does not exist. +func setFakeGid() (string, error) { + for i := GID_MIN; i <= GID_MAX; i++ { + gid := strconv.Itoa(i) + _, err := user.LookupGroupId(gid) + if err != nil { + return gid, nil + } + } + return "", fmt.Errorf("setFakeGid: could not set gid") +} + +type MockSystem struct { + mock.Mock +} + +func (m *MockSystem) AddGroup(name, gid string) error { + args := m.Called(name, gid) + return args.Error(0) +} + +func (m *MockSystem) DelGroup(name string) error { + args := m.Called(name) + return args.Error(0) +} + +func (m *MockSystem) LookupGroup(name string) (*user.Group, error) { + args := m.Called(name) + return args.Get(0).(*user.Group), args.Error(1) +} + +func (m *MockSystem) LookupGroupID(gid string) (*user.Group, error) { + args := m.Called(gid) + return args.Get(0).(*user.Group), args.Error(1) +} diff --git a/resource/group/preparer.go b/resource/group/preparer.go new file mode 100644 index 000000000..a7bacb7cb --- /dev/null +++ b/resource/group/preparer.go @@ -0,0 +1,86 @@ +// Copyright © 2016 Asteris, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group + +import ( + "fmt" + "math" + "strconv" + + "github.com/asteris-llc/converge/load/registry" + "github.com/asteris-llc/converge/resource" +) + +// Preparer for Group +// +// Group renders group data +type Preparer struct { + // Gid is the group gid. + GID string `hcl:"gid"` + + // Name is the group name. + Name string `hcl:"name"` + + // State is whether the group should be present. + State string `hcl:"state"` +} + +// Prepare a new task +func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { + gid, err := render.Render("gid", p.GID) + if err != nil { + return nil, err + } + if gid == "" { + return nil, fmt.Errorf("group requires a \"gid\" parameter") + } + gidVal, err := strconv.ParseUint(gid, 10, 32) + if err != nil { + return nil, err + } + if gidVal == math.MaxUint32 { + // the maximum gid on linux is MaxUint32 - 1 + return nil, fmt.Errorf("group \"gid\" parameter out of range") + } + + name, err := render.Render("name", p.Name) + if err != nil { + return nil, err + } + if name == "" { + return nil, fmt.Errorf("group requires a \"name\" parameter") + } + + sstate, err := render.Render("name", p.State) + state := State(sstate) + if err != nil { + return nil, err + } + if state == "" { + state = StatePresent + } else if state != StatePresent && state != StateAbsent { + return nil, fmt.Errorf("group \"state\" parameter invalid, use present or absent") + } + + grp := NewGroup(new(System)) + grp.GID = gid + grp.Name = name + grp.State = state + return grp, nil +} + +func init() { + registry.Register("group.group", (*Preparer)(nil), (*Group)(nil)) +} diff --git a/resource/group/preparer_test.go b/resource/group/preparer_test.go new file mode 100644 index 000000000..27f75ac20 --- /dev/null +++ b/resource/group/preparer_test.go @@ -0,0 +1,105 @@ +// Copyright © 2016 Asteris, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group_test + +import ( + "fmt" + "math" + "strconv" + "testing" + + "github.com/asteris-llc/converge/helpers/fakerenderer" + "github.com/asteris-llc/converge/resource" + "github.com/asteris-llc/converge/resource/group" + "github.com/stretchr/testify/assert" +) + +func TestPreparerInterface(t *testing.T) { + t.Parallel() + + assert.Implements(t, (*resource.Resource)(nil), new(group.Preparer)) +} + +func TestValidPreparer(t *testing.T) { + t.Parallel() + + fr := fakerenderer.FakeRenderer{} + p := group.Preparer{GID: "123", Name: "test", State: string(group.StateAbsent)} + _, err := p.Prepare(&fr) + + assert.NoError(t, err) +} + +func TestValidPreparerNoState(t *testing.T) { + t.Parallel() + + fr := fakerenderer.FakeRenderer{} + p := group.Preparer{GID: "123", Name: "test"} + _, err := p.Prepare(&fr) + + assert.NoError(t, err) +} + +func TestInvalidPreparerNoGid(t *testing.T) { + t.Parallel() + + fr := fakerenderer.FakeRenderer{} + p := group.Preparer{Name: "test"} + _, err := p.Prepare(&fr) + + assert.EqualError(t, err, fmt.Sprintf("group requires a \"gid\" parameter")) +} + +func TestInvalidPreparerInvalidGid(t *testing.T) { + t.Parallel() + + gid := strconv.Itoa(math.MaxUint32) + fr := fakerenderer.FakeRenderer{} + p := group.Preparer{GID: gid, Name: "test"} + _, err := p.Prepare(&fr) + + assert.EqualError(t, err, fmt.Sprintf("group \"gid\" parameter out of range")) +} + +func TestInvalidPreparerMaxGid(t *testing.T) { + t.Parallel() + + gid := strconv.Itoa(math.MaxUint32 - 1) + fr := fakerenderer.FakeRenderer{} + p := group.Preparer{GID: gid, Name: "test"} + _, err := p.Prepare(&fr) + + assert.NoError(t, err) +} + +func TestInvalidPreparerNoName(t *testing.T) { + t.Parallel() + + fr := fakerenderer.FakeRenderer{} + p := group.Preparer{GID: "123"} + _, err := p.Prepare(&fr) + + assert.EqualError(t, err, fmt.Sprintf("group requires a \"name\" parameter")) +} + +func TestInvalidPreparerInvalidState(t *testing.T) { + t.Parallel() + + fr := fakerenderer.FakeRenderer{} + p := group.Preparer{GID: "123", Name: "test", State: "test"} + _, err := p.Prepare(&fr) + + assert.EqualError(t, err, fmt.Sprintf("group \"state\" parameter invalid, use present or absent")) +} diff --git a/samples/group.hcl b/samples/group.hcl new file mode 100644 index 000000000..94dfde225 --- /dev/null +++ b/samples/group.hcl @@ -0,0 +1,5 @@ +group.group "group" { + gid = "123" + name = "test" + state = "present" +}