resource/group/group.go
// 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"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
// 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 {
// the configured group ID
GID string `export:"gid"`
// the configured group name
Name string `export:"name"`
// the desired group name
NewName string `export:"newname"`
// the group state
State State `export:"state"`
system SystemUtils
}
// ModGroupOptions are the options specified in the configuration to be used
// when modifying a group
type ModGroupOptions struct {
GID string
NewName string
}
// SystemUtils provides system utilities for group
type SystemUtils interface {
AddGroup(groupName, groupID string) error
DelGroup(groupName string) error
ModGroup(groupName string, options *ModGroupOptions) error
LookupGroup(groupName string) (*user.Group, error)
LookupGroupID(groupID string) (*user.Group, error)
}
// ErrUnsupported is used when a system is not supported
var ErrUnsupported = fmt.Errorf("group: not supported on this system")
// 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(context.Context, resource.Renderer) (resource.TaskStatus, error) {
var (
groupByGid *user.Group
gidErr error
groupByNewName *user.Group
newNameErr error
)
// lookup the group by name and lookup the group by gid
// the lookups return ErrUnsupported if the system is not supported
// LookupGroup returns user.UnknownGroupError if the group is not found
// LookupGroupID returns user.UnknownGroupIdError if the gid is not found
groupByName, nameErr := g.system.LookupGroup(g.Name)
if g.GID != "" {
groupByGid, gidErr = g.system.LookupGroupID(g.GID)
}
if g.NewName != "" {
groupByNewName, newNameErr = g.system.LookupGroup(g.NewName)
}
status := &resource.Status{}
if nameErr == ErrUnsupported {
status.RaiseLevel(resource.StatusFatal)
return status, ErrUnsupported
}
switch g.State {
case StatePresent:
switch {
case g.GID == "":
_, nameNotFound := nameErr.(user.UnknownGroupError)
switch {
case g.NewName == "":
switch {
case nameNotFound:
status.Output = append(status.Output, "add group")
status.AddDifference("group", string(StateAbsent), fmt.Sprintf("group %s", g.Name), "")
case groupByName != nil:
status.Output = append(status.Output, fmt.Sprintf("group add: group %s already exists", g.Name))
}
case g.NewName != "":
_, newNameNotFound := newNameErr.(user.UnknownGroupError)
switch {
case nameNotFound:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group modify: group %s does not exist", g.Name))
return status, errors.New("cannot modify group")
case newNameNotFound:
status.Output = append(status.Output, "modify group name")
status.AddDifference("group", fmt.Sprintf("group %s", g.Name), fmt.Sprintf("group %s", g.NewName), "")
case groupByNewName != nil:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group modify: group %s already exists", g.NewName))
return status, errors.New("cannot modify group")
}
}
case g.GID != "":
_, nameNotFound := nameErr.(user.UnknownGroupError)
_, gidNotFound := gidErr.(user.UnknownGroupIdError)
switch {
case g.NewName == "":
switch {
case nameNotFound && gidNotFound:
status.Output = append(status.Output, "add group with gid")
status.AddDifference("group", string(StateAbsent), fmt.Sprintf("group %s with gid %s", g.Name, g.GID), "")
case nameNotFound:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group add: gid %s already exists", g.GID))
return status, errors.New("cannot add group")
case gidNotFound:
status.Output = append(status.Output, "modify group gid")
status.AddDifference("group", fmt.Sprintf("group %s with gid %s", g.Name, groupByName.Gid), fmt.Sprintf("group %s with gid %s", g.Name, g.GID), "")
case groupByName != nil && groupByGid != nil && groupByName.Name != groupByGid.Name || groupByName.Gid != groupByGid.Gid:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group add/modify: group %s and gid %s belong to different groups", g.Name, g.GID))
return status, errors.New("cannot add or modify group")
case groupByName != nil && groupByGid != nil && *groupByName == *groupByGid:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group add/modify: group %s with gid %s already exists", g.Name, g.GID))
return status, errors.New("cannot add or modify group")
}
case g.NewName != "":
_, newNameNotFound := newNameErr.(user.UnknownGroupError)
switch {
case newNameNotFound && gidNotFound:
status.Output = append(status.Output, "modify group name and gid")
status.AddDifference("group", fmt.Sprintf("group %s with gid %s", g.Name, groupByName.Gid), fmt.Sprintf("group %s with gid %s", g.NewName, g.GID), "")
case gidNotFound:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group modify: group %s already exists", g.NewName))
return status, errors.New("cannot modify group")
case newNameNotFound:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group modify: gid %s already exists", g.GID))
return status, errors.New("cannot modify group")
}
}
}
case StateAbsent:
switch {
case g.GID == "":
_, nameNotFound := nameErr.(user.UnknownGroupError)
switch {
case nameNotFound:
status.Output = append(status.Output, fmt.Sprintf("group delete: group %s does not exist", g.Name))
case groupByName != nil:
status.Output = append(status.Output, "delete group")
status.AddDifference("group", fmt.Sprintf("group %s", g.Name), string(StateAbsent), "")
}
case g.GID != "":
_, nameNotFound := nameErr.(user.UnknownGroupError)
_, gidNotFound := gidErr.(user.UnknownGroupIdError)
switch {
case nameNotFound && gidNotFound:
status.Output = append(status.Output, fmt.Sprintf("group delete: group %s and gid %s do not exist", g.Name, g.GID))
case nameNotFound:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group delete: group %s does not exist", g.Name))
return status, errors.New("cannot delete group")
case gidNotFound:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group delete: gid %s does not exist", g.GID))
return status, errors.New("cannot delete group")
case groupByName != nil && groupByGid != nil && groupByName.Name != groupByGid.Name || groupByName.Gid != groupByGid.Gid:
status.RaiseLevel(resource.StatusCantChange)
status.Output = append(status.Output, fmt.Sprintf("group delete: group %s and gid %s belong to different groups", g.Name, g.GID))
return status, errors.New("cannot delete group")
case groupByName != nil && groupByGid != nil && *groupByName == *groupByGid:
status.Output = append(status.Output, "delete group with gid")
status.AddDifference("group", fmt.Sprintf("group %s with gid %s", g.Name, g.GID), string(StateAbsent), "")
}
}
default:
status.RaiseLevel(resource.StatusFatal)
return status, fmt.Errorf("group: unrecognized state %s", g.State)
}
status.RaiseLevelForDiffs()
return status, nil
}
// Apply changes for group
func (g *Group) Apply(context.Context) (resource.TaskStatus, error) {
var (
groupByGid *user.Group
gidErr error
newNameErr error
)
// lookup the group by name and lookup the group by gid
// the lookups return ErrUnsupported if the system is not supported
// LookupGroup returns user.UnknownGroupError if the group is not found
// LookupGroupID returns user.UnknownGroupIdError if the gid is not found
groupByName, nameErr := g.system.LookupGroup(g.Name)
if g.GID != "" {
groupByGid, gidErr = g.system.LookupGroupID(g.GID)
}
if g.NewName != "" {
_, newNameErr = g.system.LookupGroup(g.NewName)
}
status := &resource.Status{}
if nameErr == ErrUnsupported {
status.RaiseLevel(resource.StatusFatal)
return status, ErrUnsupported
}
switch g.State {
case StatePresent:
switch {
case g.GID == "":
_, nameNotFound := nameErr.(user.UnknownGroupError)
switch {
case g.NewName == "":
switch {
case nameNotFound:
err := g.system.AddGroup(g.Name, g.GID)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
status.Output = append(status.Output, fmt.Sprintf("error adding group %s", g.Name))
return status, errors.Wrap(err, "group add")
}
status.Output = append(status.Output, fmt.Sprintf("added group %s", g.Name))
default:
status.RaiseLevel(resource.StatusCantChange)
return status, fmt.Errorf("will not attempt add: group %s", g.Name)
}
case g.NewName != "":
_, newNameNotFound := newNameErr.(user.UnknownGroupError)
switch {
case groupByName != nil && newNameNotFound:
options := SetModGroupOptions(g)
err := g.system.ModGroup(g.Name, options)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
status.Output = append(status.Output, fmt.Sprintf("error modifying group %s", g.Name))
return status, errors.Wrap(err, "group modify")
}
status.Output = append(status.Output, fmt.Sprintf("modified group %s with new name %s", g.Name, g.NewName))
default:
status.RaiseLevel(resource.StatusCantChange)
return status, fmt.Errorf("will not attempt modify: group %s", g.Name)
}
}
case g.GID != "":
_, nameNotFound := nameErr.(user.UnknownGroupError)
_, gidNotFound := gidErr.(user.UnknownGroupIdError)
switch {
case g.NewName == "":
switch {
case nameNotFound && gidNotFound:
err := g.system.AddGroup(g.Name, g.GID)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
status.Output = append(status.Output, fmt.Sprintf("error adding group %s with gid %s", g.Name, g.GID))
return status, errors.Wrap(err, "group add")
}
status.Output = append(status.Output, fmt.Sprintf("added group %s with gid %s", g.Name, g.GID))
case gidNotFound:
options := SetModGroupOptions(g)
err := g.system.ModGroup(g.Name, options)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
status.Output = append(status.Output, fmt.Sprintf("error modifying group %s with new gid %s", g.Name, g.GID))
return status, errors.Wrap(err, "group modify")
}
status.Output = append(status.Output, fmt.Sprintf("modified group %s with new gid %s", g.Name, g.GID))
default:
status.RaiseLevel(resource.StatusCantChange)
return status, fmt.Errorf("will not attempt add/modify: group %s with gid %s", g.Name, g.GID)
}
case g.NewName != "":
_, newNameNotFound := newNameErr.(user.UnknownGroupError)
switch {
case groupByName != nil && newNameNotFound && gidNotFound:
options := SetModGroupOptions(g)
err := g.system.ModGroup(g.Name, options)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
status.Output = append(status.Output, fmt.Sprintf("error modifying group %s with new name %s and new gid %s", g.Name, g.NewName, g.GID))
return status, errors.Wrap(err, "group modify")
}
status.Output = append(status.Output, fmt.Sprintf("modified group %s with new name %s and new gid %s", g.Name, g.NewName, g.GID))
default:
status.RaiseLevel(resource.StatusCantChange)
return status, fmt.Errorf("will not attempt modify: group %s with new name %s and new gid %s", g.Name, g.NewName, g.GID)
}
}
}
case StateAbsent:
switch {
case g.GID == "":
_, nameNotFound := nameErr.(user.UnknownGroupError)
switch {
case !nameNotFound && groupByName != nil:
err := g.system.DelGroup(g.Name)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
status.Output = append(status.Output, fmt.Sprintf("error deleting group %s", g.Name))
return status, errors.Wrap(err, "group delete")
}
status.Output = append(status.Output, fmt.Sprintf("deleted group %s", g.Name))
default:
status.RaiseLevel(resource.StatusCantChange)
return status, fmt.Errorf("will not attempt delete: group %s", g.Name)
}
case g.GID != "":
_, nameNotFound := nameErr.(user.UnknownGroupError)
_, gidNotFound := gidErr.(user.UnknownGroupIdError)
switch {
case !nameNotFound && !gidNotFound && groupByName != nil && groupByGid != nil && *groupByName == *groupByGid:
err := g.system.DelGroup(g.Name)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
status.Output = append(status.Output, fmt.Sprintf("error deleting group %s with gid %s", g.Name, g.GID))
return status, errors.Wrap(err, "group delete")
}
status.Output = append(status.Output, fmt.Sprintf("deleted group %s with gid %s", g.Name, g.GID))
default:
status.RaiseLevel(resource.StatusCantChange)
return status, fmt.Errorf("will not attempt delete: group %s with gid %s", g.Name, g.GID)
}
}
default:
status.RaiseLevel(resource.StatusFatal)
return status, fmt.Errorf("group: unrecognized state %s", g.State)
}
return status, nil
}
// SetModGroupOptions returns a ModGroupOptions struct with the options
// specified in the configuration for modifying a group
func SetModGroupOptions(g *Group) *ModGroupOptions {
options := new(ModGroupOptions)
if g.GID != "" {
options.GID = g.GID
}
if g.NewName != "" {
options.NewName = g.NewName
}
return options
}