Browse Source

Update HCL config and add smf manifest generation

layerset
Till Wegmueller 3 years ago
parent
commit
c2fdb42b38
  1. 35
      cmd/buildhelper.go
  2. 13
      cmd/imageadm/build.go
  3. 5
      go.mod
  4. 10
      go.sum
  5. 6
      host/build.go
  6. 26
      image/build/build.go
  7. 82
      image/build/config.go
  8. 25
      smf/manifest.go
  9. 5
      supportfiles/image-build-files/registry.hcl
  10. 27
      sysacct/user.go

35
cmd/buildhelper.go

@ -68,21 +68,40 @@ func main() {
}
if imageConfig.Command != nil {
if imageConfig.Command.User == nil {
imageConfig.Command.User = &sysacct.User{
Name: "process",
Gecos: "User to execute the Process in the Image",
Group: "process",
if imageConfig.RunAsUser == "process" {
imageConfig.RunAsUID = 1000
}
if imageConfig.RunAsGroup == "process" {
imageConfig.RunAsGID = 100
}
if _, err := os.Stat("/var/oci"); err != nil {
if os.IsNotExist(err) {
if err = os.Mkdir("/var/oci", 750); err != nil {
panic(err)
}
} else {
panic(err)
}
}
logrus.Infof("Adding Process User %s to image", imageConfig.Command.User)
u, err := sysacct.UserAdd(*imageConfig.Command.User)
logrus.Infof("Adding Process User %s to image", imageConfig.RunAsUser)
u, err := sysacct.UserAdd(sysacct.User{
Name: imageConfig.RunAsUser,
Gecos: "The user to run the container process as",
Group: imageConfig.RunAsGroup,
HomeDir: "/var/oci",
UID: imageConfig.RunAsGID,
GID: imageConfig.RunAsGID,
AdditionalGroups: imageConfig.RunAsAdditionalGroups,
})
if err != nil {
panic(err)
}
imageConfig.Command.User = &u
imageConfig.RunAsAdditionalGIDs = u.AdditionalGIDs
logrus.Infof("injecting command into image")
if err := build.CreateProcessInZoneSMF(&imageConfig); err != nil {
panic(err)

13
cmd/imageadm/build.go

@ -1,6 +1,9 @@
package imageadm
import (
"path/filepath"
"strings"
"git.wegmueller.it/opencloud/opencloud/image/build"
"git.wegmueller.it/opencloud/opencloud/net"
"git.wegmueller.it/opencloud/opencloud/pod"
@ -31,6 +34,16 @@ var buildCmd = &cobra.Command{
Context: "./",
}
absCTXPath, err := filepath.Abs(buildOpts.Context)
if err != nil {
return tracerr.Wrap(err)
}
// Clean out context path if we point to a absolute path that begins with the context path
if strings.HasPrefix(fname, absCTXPath) {
buildOpts.Context = ""
}
podOpts := pod.Options{}
if len(args) > 0 {

5
go.mod

@ -18,12 +18,13 @@ require (
github.com/docker/go-metrics v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/garyburd/redigo v1.6.0 // indirect
github.com/go-test/deep v1.0.1
github.com/go-test/deep v1.0.3
github.com/gofrs/uuid v3.2.0+incompatible // indirect
github.com/goodhosts/hostsfile v0.0.1 // indirect
github.com/gorilla/handlers v1.4.2 // indirect
github.com/gotestyourself/gotestyourself v1.4.0 // indirect
github.com/h2non/filetype v1.0.5
github.com/hashicorp/hcl/v2 v2.4.0
github.com/hashicorp/hcl2 v0.0.0-20190515223218-4b22149b7cef
github.com/jstemmer/go-junit-report v0.9.1
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
@ -60,3 +61,5 @@ require (
replace github.com/sirupsen/logrus => github.com/sirupsen/logrus v1.4.2-0.20190403091019-9b3cdde74fbe
replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20200319173657-742aab907b54
replace github.com/goodhosts/hostsfile => github.com/Toasterson/hostsfile v0.0.2-0.20200421083026-d1b63f2bfda6

10
go.sum

@ -66,6 +66,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/Toasterson/hostsfile v0.0.2-0.20200421083026-d1b63f2bfda6 h1:B4VUrmkHWSBjlKJBJZluGjV1wf+03Ur71aK6dhXNF64=
github.com/Toasterson/hostsfile v0.0.2-0.20200421083026-d1b63f2bfda6/go.mod h1:gdyZoxeaK1NAmzDM9OpP3H6aQQL1OzrMg3RTsIOiL3Q=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
@ -83,6 +85,8 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/appc/spec v0.8.11 h1:BFwMCTHSDwanDlAA3ONbsLllTw4pCW85kVm290dNrV4=
github.com/appc/spec v0.8.11/go.mod h1:2F+EK25qCkHIzwA7HQjWIK7r2LOL1gQlou8mm2Fdif0=
github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM=
@ -199,6 +203,7 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gofrs/uuid v1.2.0 h1:coDhrjgyJaglxSjxuJdqQSSdUpG3w6p1OwN2od6frBU=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -232,6 +237,7 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@ -284,6 +290,8 @@ github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGE
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.4.0 h1:xwVa1aj4nCSoAjUnFPBAIfqlzPgSZEVMdkJv/mgj4jY=
github.com/hashicorp/hcl/v2 v2.4.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/hashicorp/hcl2 v0.0.0-20190515223218-4b22149b7cef h1:xZRvbcwHY8zhaxDwgkmpAp2emwZkVn7p3gat0zhq2X0=
github.com/hashicorp/hcl2 v0.0.0-20190515223218-4b22149b7cef/go.mod h1:4oI94iqF3GB10QScn46WqbG0kgTUpha97SAzzg2+2ec=
github.com/heroku/docker-registry-client v0.0.0-20181004091502-47ecf50fd8d4 h1:44WMsEqwiYnpHA3E4Rg1K379MH5iZllp2sO5nzXARI0=
@ -534,6 +542,8 @@ github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9 h1
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
github.com/zclconf/go-cty v0.0.0-20190426224007-b18a157db9e2 h1:Ai1LhlYNEqE39zGU07qHDNJ41iZVPZfZr1dSCoXrp1w=
github.com/zclconf/go-cty v0.0.0-20190426224007-b18a157db9e2/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/ztrue/tracerr v0.3.0 h1:lDi6EgEYhPYPnKcjsYzmWw4EkFEoA/gfe+I9Y5f+h6Y=
github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=

6
host/build.go

@ -322,14 +322,14 @@ func (h *Host) ExportToImage(ref reference.Reference, container *pod.Container,
}
specImageConfig := specsv1.ImageConfig{
User: imageConfig.CommandUser,
ExposedPorts: toStringStruct(imageConfig.ExposedPorts),
Env: toStringSlice(imageConfig.Environment, "="),
Entrypoint: imageConfig.EntryPoint,
Cmd: imageConfig.Command,
WorkingDir: imageConfig.WorkingDir,
Labels: imageConfig.Labels,
Cmd: imageConfig.Command,
Volumes: volumeConfigToStringStruct(imageConfig.Volumes),
WorkingDir: imageConfig.WorkingDirectory,
User: imageConfig.RunAsUser,
}
wr.AddImageConfig(specImageConfig)

26
image/build/build.go

@ -376,7 +376,7 @@ func CreateEntryPointInZone(cfg *Image) error {
}
fullPath := filepath.Join("/lib/svc/manifest/application/entrypoint.xml")
manifestObj := smf.NewManifestFromOCIProcess("oci-entrypoint", smf.OCIEntryPointSVCName, cfg.Command.Milestone, p)
manifestObj := smf.NewManifestFromOCIProcess("oci-entrypoint", smf.OCIEntryPointSVCName, cfg.Milestone, p)
manifestObj.Services[0].Dependent = append(manifestObj.Services[0].Dependent, smf.Dependent{
Name: "oci-process",
Grouping: "require_all",
@ -393,6 +393,10 @@ func CreateEntryPointInZone(cfg *Image) error {
}
defer manFile.Close()
if _, err := manFile.WriteString(smf.Header); err != nil {
return tracerr.Wrap(err)
}
enc := xml.NewEncoder(manFile)
enc.Indent("", " ")
if err := enc.Encode(manifestObj); err != nil {
@ -412,17 +416,21 @@ func CreateProcessInZoneSMF(cfg *Image) error {
}
p := specs.Process{
Terminal: false,
Env: toStringSlice(cfg.Environment, "="),
Args: cfg.Command.Args,
Cwd: cfg.Command.WorkingDir,
User: specs.User{UID: cfg.Command.User.UID, GID: cfg.Command.User.GID, AdditionalGids: cfg.Command.User.AdditionalGIDs},
Terminal: false,
Env: toStringSlice(cfg.Environment, "="),
Args: cfg.Command,
User: specs.User{
UID: cfg.RunAsUID,
GID: cfg.RunAsGID,
AdditionalGids: cfg.RunAsAdditionalGIDs,
},
NoNewPrivileges: true,
Cwd: cfg.WorkingDirectory,
}
fullPath := filepath.Join("/lib/svc/manifest/application/oci-process.xml")
manifestObj := smf.NewManifestFromOCIProcess("oci-process", smf.OCIProcesSVCName, cfg.Command.Milestone, p)
manifestObj := smf.NewManifestFromOCIProcess("oci-process", smf.OCIProcesSVCName, cfg.Milestone, p)
if cfg.ExposedPorts != nil {
needsPrivPorts := false
@ -447,6 +455,10 @@ func CreateProcessInZoneSMF(cfg *Image) error {
}
defer manFile.Close()
if _, err := manFile.WriteString(smf.Header); err != nil {
return tracerr.Wrap(err)
}
enc := xml.NewEncoder(manFile)
enc.Indent("", " ")
if err := enc.Encode(manifestObj); err != nil {

82
image/build/config.go

@ -7,9 +7,8 @@ import (
"git.wegmueller.it/opencloud/opencloud/sysacct"
"git.wegmueller.it/opencloud/opencloud/zfs"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hclparse"
hcl2 "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/ztrue/tracerr"
)
@ -25,7 +24,7 @@ const (
type ImageConfig struct {
Maintainer *MaintainerInformation `hcl:"maintainer"`
Images []Image `hcl:"image,block"`
Body hcl.Body `hcl:",remain"`
Body hcl2.Body `hcl:",remain"`
}
func (c *ImageConfig) PostDecode() {
@ -74,35 +73,48 @@ func (c *ImageConfig) PostDecode() {
img.OSFlavour = runtime.GOOS
}
if img.RunAsUser == "" {
img.RunAsUser = "process"
}
if img.RunAsGroup == "" {
img.RunAsGroup = "process"
}
if img.Milestone == "" {
img.Milestone = "svc:/milestone/multi-user"
}
c.Images[idx] = img
}
}
type Image struct {
Name string `hcl:"name,label"`
BaseImage string `hcl:"base"`
OSFlavour string `hcl:"os,optional"`
Users []sysacct.User `hcl:"user,block"`
Groups []sysacct.Group `hcl:"group,block"`
Sources []SourceDescription `hcl:"source,block"`
CopyOperations []CopyOperation `hcl:"copy,block"`
Actions []Action `hcl:"action,block"`
Maintainer *MaintainerInformation `hcl:"maintainer,optional"`
Services []ServiceDescription `hcl:"service,block"` //TODO implement
Command *Command `hcl:"cmd,optional"`
ExposedPorts []string `hcl:"expose,optional"`
Environment map[string]string `hcl:"env,optional"`
EntryPoint []string `hcl:"entrypoint,optional"`
Volumes []VolumeConfig `hcl:"volume,block"`
Hosts []HostEntry `hcl:"host,block"`
Labels map[string]string `hcl:"labels,optional"`
}
type Command struct {
Args []string `hcl:"cmd"` //Binary and arguments to run
User *sysacct.User `hcl:"user,optional"` //User to run the command as
WorkingDir string `hcl:"workdir,optional"` //Directory to chdir into before starting the process defaults to /
Milestone string `hcl:"milestone,optional"` //The milestone after which the process should start defaults to svc:/milestone/multi-user must be an fmri
Name string `hcl:"name,label"`
BaseImage string `hcl:"base"`
OSFlavour string `hcl:"os,optional"`
Users []sysacct.User `hcl:"user,block"`
Groups []sysacct.Group `hcl:"group,block"`
Sources []SourceDescription `hcl:"source,block"`
CopyOperations []CopyOperation `hcl:"copy,block"`
Actions []Action `hcl:"action,block"`
Maintainer *MaintainerInformation `hcl:"maintainer"`
Services []ServiceDescription `hcl:"service,block"` //TODO implement
Command []string `hcl:"cmd,optional"`
WorkingDirectory string `hcl:"workdir,optional"`
Milestone string `hcl:"milestone,optional"`
RunAsUser string `hcl:"runas,optional"`
RunAsGroup string `hcl:"runas_user,optional"`
RunAsUID uint32 `hcl:"runas_uid,optional"`
RunAsGID uint32 `hcl:"runas_gid,optional"`
RunAsAdditionalGroups []string `hcl:"runas_additional_groups,optional"`
RunAsAdditionalGIDs []uint32 `hcl:"runas_additional_gids,optional"`
ExposedPorts []string `hcl:"expose,optional"`
Environment map[string]string `hcl:"env,optional"`
EntryPoint []string `hcl:"entrypoint,optional"`
Volumes []VolumeConfig `hcl:"volume,block"`
Hosts []HostEntry `hcl:"host,block"`
Labels map[string]string `hcl:"labels,optional"`
}
type ServiceDescription struct {
@ -159,19 +171,11 @@ type SourceDescription struct {
}
func ReadConfigFile(path string) (*ImageConfig, error) {
parser := hclparse.NewParser()
f, diags := parser.ParseHCLFile(path)
if diags != nil && diags.HasErrors() {
return nil, tracerr.Wrap(diags)
}
return ReadConfig(f)
}
func ReadConfig(blob *hcl.File) (*ImageConfig, error) {
var img ImageConfig
if diags := gohcl.DecodeBody(blob.Body, nil, &img); diags != nil && diags.HasErrors() {
return nil, tracerr.Wrap(diags)
if err := hclsimple.DecodeFile(path, nil, &img); err != nil {
return nil, tracerr.Wrap(err)
}
img.PostDecode()
return &img, nil
}

25
smf/manifest.go

@ -7,8 +7,11 @@ import (
"github.com/opencontainers/runtime-spec/specs-go"
)
const OCIProcesSVCName = "svc:/application/oci-process"
const OCIEntryPointSVCName = "svc:/application/oci-entrypoint"
const OCIProcesSVCName = "application/oci-process"
const OCIEntryPointSVCName = "application/oci-entrypoint"
const Header = `<?xml version="1.0"?>
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
`
func NewManifest(name string) ServiceBundle {
return ServiceBundle{
@ -67,7 +70,9 @@ func NewManifestFromOCIProcess(bundleName, svcName string, milestone string, pro
},
},
},
Name: svcName,
Name: svcName,
Version: "1",
Type: "service",
CreateDefaultInstance: &CreateDefaultInstance{
Enabled: true,
},
@ -84,14 +89,16 @@ func NewManifestFromOCIProcess(bundleName, svcName string, milestone string, pro
},
ExecMethod: []ExecMethod{
{
Type: "method",
Name: "start",
Exec: strings.Join(process.Args, " "),
Type: "method",
Name: "start",
Exec: strings.Join(process.Args, " "),
TimeoutSeconds: "0",
},
{
Type: "method",
Name: "stop",
Exec: ":kill",
Type: "method",
Name: "stop",
Exec: ":kill",
TimeoutSeconds: "0",
},
},
PropertyGroup: []PropertyGroup{

5
supportfiles/image-build-files/registry.hcl

@ -46,7 +46,8 @@ image "aurora-opencloud/registry" {
description = "Registry layer directory"
}
expose = [ "5000" ]
workdir = "/"
cmd = ["/registry", "--config", "/registry_config.yml" ]
workdir = "/"
expose = [ "5000" ]
}

27
sysacct/user.go

@ -13,14 +13,15 @@ import (
const userAddBin = "/usr/sbin/useradd"
type User struct {
Name string `hcl:"name"`
Gecos string `hcl:"gecos,label"`
Group string `hcl:"group,optional"`
Shell string `hcl:"shell,optional"`
UID uint32 `hcl:"uid,optional"`
GID uint32 `hcl:"-,optional"`
AdditionalGroups []string `hcl:"groups,optional"`
AdditionalGIDs []uint32 `hcl:"-,optional"`
Name string
Gecos string
Group string
Shell string
UID uint32
GID uint32
HomeDir string
AdditionalGroups []string
AdditionalGIDs []uint32
}
func UserAdd(u User) (User, error) {
@ -85,11 +86,11 @@ func UserAdd(u User) (User, error) {
u.AdditionalGIDs = append(u.AdditionalGIDs, uint32(i))
} else {
if err := GroupAdd(Group{Name: u.Group}); err != nil {
if err := GroupAdd(Group{Name: g}); err != nil {
return u, tracerr.Wrap(err)
}
lookupGroup, _ = user.LookupGroup(u.Group)
lookupGroup, _ = user.LookupGroup(g)
i, err := strconv.Atoi(lookupGroup.Gid)
if err != nil {
return u, tracerr.Wrap(err)
@ -100,10 +101,14 @@ func UserAdd(u User) (User, error) {
}
}
if u.UID > 0 {
if u.UID != 0 {
args = append(args, "-u", strconv.Itoa(int(u.UID)))
}
if u.HomeDir != "" {
args = append(args, "-m", "-d", u.HomeDir)
}
args = append(args, u.Name)
c := exec.Command(userAddBin, args...)

Loading…
Cancel
Save