diff --git a/go.mod b/go.mod index 63283d5..b259c94 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/go-openapi/validate v0.20.2 github.com/jessevdk/go-flags v1.5.0 github.com/kraken-hpc/go-fork v0.1.1 - github.com/kraken-hpc/uinit v0.1.1 + github.com/kraken-hpc/uinit v0.2.0 github.com/mailru/easyjson v0.7.7 // indirect github.com/sirupsen/logrus v1.8.1 github.com/u-root/iscsinl v0.1.0 diff --git a/go.sum b/go.sum index 4881857..a10ef2a 100644 --- a/go.sum +++ b/go.sum @@ -22,7 +22,6 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= -github.com/bensallen/rbd v0.0.0-20201123204607-9b994a7784d0/go.mod h1:scQzzcMu4X4w0L6rELxUERr68HS2Lqu/NYdnxw72sV8= github.com/bensallen/rbd v0.0.0-20210224155049-baf486eceefa h1:fk3cRl+COO4Ps5eiMTFD2VIve8PN3yfLztDDsuKUZjI= github.com/bensallen/rbd v0.0.0-20210224155049-baf486eceefa/go.mod h1:2CDopFVfYqEf77T3xHJDcvuCULhkdQ4HgtstEVoPtfM= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -66,7 +65,6 @@ github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9sn github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk= -github.com/go-openapi/analysis v0.20.0 h1:UN09o0kNhleunxW7LR+KnltD0YrJ8FF03pSqvAN3Vro= github.com/go-openapi/analysis v0.20.0/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og= github.com/go-openapi/analysis v0.20.1 h1:zdVbw8yoD4SWZeq+cWdGgquaB0W4VrsJvDJHJND/Ktc= github.com/go-openapi/analysis v0.20.1/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og= @@ -90,7 +88,6 @@ github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3Hfo github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= @@ -221,11 +218,9 @@ github.com/google/goexpect v0.0.0-20200816234442-b5b77125c2c5/go.mod h1:n1ej5+Fq github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/hexdigest/gowrap v1.1.7/go.mod h1:Z+nBFUDLa01iaNM+/jzoOA1JJ7sm51rnYFauKFUB5fs= github.com/hexdigest/gowrap v1.1.8/go.mod h1:H/JiFmQMp//tedlV8qt2xBdGzmne6bpbaSuiHmygnMw= @@ -234,6 +229,8 @@ github.com/insomniacslk/dhcp v0.0.0-20200814125043-2e1bf785d039/go.mod h1:CfMdgu github.com/intel-go/cpuid v0.0.0-20200819041909-2aa72927c3e2/go.mod h1:RmeVYf9XrPRbRc3XIx0gLYA8qOFvNoPOfaEZduRlEp4= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w= +github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= github.com/jlowellwofford/iscsinl v0.1.1-0.20210831201708-527d3dbd2e6d h1:SU2fb/eqGE6dawxfiHVzMKM6Hb4z4iGEn3VqQP+Fuws= github.com/jlowellwofford/iscsinl v0.1.1-0.20210831201708-527d3dbd2e6d/go.mod h1:RWIgJWqm9/0gjBZ0Hl8iR6MVGzZ+yAda2uqqLmetE2I= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -264,8 +261,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kraken-hpc/go-fork v0.1.1 h1:O3X/ynoNy/eS7UIcZYef8ndFq2RXEIOue9kZqyzF0Sk= github.com/kraken-hpc/go-fork v0.1.1/go.mod h1:uu0e5h+V4ONH5Qk/xuVlyNXJXy/swhqGIEMK7w+9dNc= -github.com/kraken-hpc/uinit v0.1.1 h1:gdKNgb0NQDU73mZKs4qfb0N2eY7q1K6jFaNe2f/2pTo= -github.com/kraken-hpc/uinit v0.1.1/go.mod h1:S09qeh87rhAU14nENTRIggeF/tC6n+QJAYvW11UP4HI= +github.com/kraken-hpc/uinit v0.2.0 h1:9yaSoRRN1tm4SckUktvstU8L7qR0OsyL/saMCGy+nN0= +github.com/kraken-hpc/uinit v0.2.0/go.mod h1:O9SICS/svBXcKFOzee7ZjwlQX+Cr0eafUlsR0eBKWXQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -309,7 +306,6 @@ github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUr github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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= @@ -343,7 +339,6 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -388,7 +383,6 @@ go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= -go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI= go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= go.mongodb.org/mongo-driver v1.7.0 h1:hHrvOBWlWB2c7+8Gh/Xi5jj82AgidK/t7KVXBZ+IyUA= go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= @@ -486,7 +480,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -499,7 +492,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -519,8 +511,6 @@ golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200915201639-f4cefd1cb5ba/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201119174615-0557df368a99/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= @@ -543,7 +533,6 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= diff --git a/internal/api/container.go b/internal/api/container.go index c677ca0..55e35a6 100644 --- a/internal/api/container.go +++ b/internal/api/container.go @@ -110,7 +110,7 @@ func (c *Containers) Create(n *Container) (ret *Container, err error) { // set up logger if err = os.MkdirAll(API.LogDir, 0700); err != nil { l.WithError(err).Error("could not make log directory") - return nil, fmt.Errorf("could not create log directory: %v", err) + return nil, ErrSrv } ctn.Logfile = path.Join(API.LogDir, fmt.Sprintf("%d-%d.log", ctn.ID, time.Now().Unix())) f, err := os.Create(ctn.Logfile) @@ -121,6 +121,29 @@ func (c *Containers) Create(n *Container) (ret *Container, err error) { n.log = log.New(f, fmt.Sprintf("container(%d): ", ctn.ID), log.Ldate|log.Ltime|log.Lmsgprefix) n.log.Printf("container created") + n.log.Printf("running script hook: create") + var hook *models.ContainerScriptHook + if ctn.Hooks != nil { + hook = ctn.Hooks.Create + } + if h, err := NewHook(hook, "", ctn.Mount.Mountpoint, n.log, map[string]string{ + "logfile": ctn.Logfile, + "command": *ctn.Command, + "mountpoint": ctn.Mount.Mountpoint, + "mountkind": ctn.Mount.Kind, + "name": string(ctn.Name), + "id": fmt.Sprintf("%d", ctn.ID), + "systemd": fmt.Sprintf("%t", ctn.Systemd), + }); err != nil { + l.WithError(err).Debug("fatal error building create scripts") + return nil, ErrFail + } else { + if err = h.Run(); err != nil { + l.WithError(err).Debug("fatal error running create scripts") + return nil, ErrFail + } + } + // handle initial state switch ctn.State { case models.ContainerStateRunning: @@ -215,6 +238,31 @@ func (c *Containers) Delete(id models.ID) (ret *Container, err error) { l.Trace("attempt to delete stopping container") return nil, ErrBusy } + // run delete hook + ctn.log.Printf("running script hook: exit") + var hook *models.ContainerScriptHook + if ctn.Container.Hooks != nil { + hook = ctn.Container.Hooks.Exit + } + if h, err := NewHook(hook, "", ctn.Container.Mount.Mountpoint, ctn.log, map[string]string{ + "logfile": ctn.Container.Logfile, + "command": *ctn.Container.Command, + "mountpoint": ctn.Container.Mount.Mountpoint, + "mountkind": ctn.Container.Mount.Kind, + "name": string(ctn.Container.Name), + "id": fmt.Sprintf("%d", ctn.Container.ID), + "systemd": fmt.Sprintf("%t", ctn.Container.Systemd), + "state": string(ctn.Container.State), + }); err != nil { + l.WithError(err).Debug("fatal error building exit scripts") + // but we don't actually do anything here + } else { + if err = h.Run(); err != nil { + l.WithError(err).Debug("fatal error running exit scripts") + // but we don't actually do anything here + } + } + ctn.log.Printf("container deleted") ctn.log.Writer().(io.WriteCloser).Close() if ctn.Container.Name != "" { @@ -277,7 +325,7 @@ func (c *Containers) run(ctn *Container) (err error) { // 2 parse command into args args := uinit.SplitCommandLine(*ctn.Container.Command) if len(args) < 1 { - return fmt.Errorf("command appears to be invalid: %s", *ctn.Container.Command) + return fmt.Errorf("clone: command appears to be invalid: %s", *ctn.Container.Command) } // 3. Is our init valid? @@ -286,7 +334,26 @@ func (c *Containers) run(ctn *Container) (err error) { return fmt.Errorf("clone: init validationfailed: %v", err) } - // 3. Launch new process + // 4. Pre-build start hook + log.Print("building script hook: Init") + var hook *models.ContainerScriptHook + if ctn.Container.Hooks != nil { + hook = ctn.Container.Hooks.Init + } + h, err := NewHook(hook, "", ctn.Container.Mount.Mountpoint, ctn.log, map[string]string{ + "logfile": ctn.Container.Logfile, + "command": *ctn.Container.Command, + "mountpoint": ctn.Container.Mount.Mountpoint, + "mountkind": ctn.Container.Mount.Kind, + "name": string(ctn.Container.Name), + "id": fmt.Sprintf("%d", ctn.Container.ID), + "systemd": fmt.Sprintf("%t", ctn.Container.Systemd), + }) + if err != nil { + return fmt.Errorf("clone: fatal error loading init script hook: %v", err.Error()) + } + + // 5. Launch new process f := fork.NewFork("containerInit", containerInit) f.Stdout = log.Writer().(*os.File) f.Stderr = log.Writer().(*os.File) @@ -294,7 +361,7 @@ func (c *Containers) run(ctn *Container) (err error) { f.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWPID | syscall.CLONE_NEWIPC | syscall.CLONE_NEWUTS, } - if err := f.Fork(ctn.Container.Mount.Mountpoint, ctn.Container.Systemd, args); err != nil { + if err := f.Fork(ctn.Container.Mount.Mountpoint, ctn.Container.Systemd, h, args); err != nil { return fmt.Errorf("clone: failed to start pid_init: %v", err) } @@ -359,7 +426,7 @@ var specialLinks = []symlinkType{ } // this is run as a separate process -func containerInit(mountpoint string, systemd bool, args []string) { +func containerInit(mountpoint string, systemd bool, hook *Hook, args []string) { // 0. setup logging l := log.New(os.Stdout, "init: ", log.Ldate|log.Ltime|log.Lmsgprefix) @@ -404,7 +471,15 @@ func containerInit(mountpoint string, systemd bool, args []string) { } } - // 6. execute init + // 6. Run init script hooks + if hook != nil { + l.Printf("executing init script hooks") + if err := hook.Run(); err != nil { + l.Fatalf("fatal init script hook error: %v", err) + } + } + + // 7. execute init l.Print("executing init") if err := unix.Exec(args[0], args, []string{}); err != nil { l.Fatalf("containerInit: exec failed: %v", err) @@ -424,8 +499,9 @@ func (c *Containers) watcher(ctx context.Context, ctn *Container, f *fork.Functi end <- e }() state := models.ContainerStateExited + var e error select { - case e := <-end: + case e = <-end: if e != nil { l.WithError(e).Debug("process ended in error state") ctn.log.Printf("process ended in error state: %v", e) @@ -452,6 +528,37 @@ func (c *Containers) watcher(ctx context.Context, ctn *Container, f *fork.Functi unix.Syncfs(fd) unix.Close(fd) } + + // run the Exit hook + ctn.log.Printf("running script hook: exit") + var hook *models.ContainerScriptHook + if ctn.Container.Hooks != nil { + hook = ctn.Container.Hooks.Exit + } + errstr := "" + if e != nil { + errstr = e.Error() + } + if h, err := NewHook(hook, "", ctn.Container.Mount.Mountpoint, ctn.log, map[string]string{ + "logfile": ctn.Container.Logfile, + "command": *ctn.Container.Command, + "mountpoint": ctn.Container.Mount.Mountpoint, + "mountkind": ctn.Container.Mount.Kind, + "name": string(ctn.Container.Name), + "id": fmt.Sprintf("%d", ctn.Container.ID), + "systemd": fmt.Sprintf("%t", ctn.Container.Systemd), + "error": fmt.Sprintf("%t", e != nil), + "errorstring": errstr, + }); err != nil { + l.WithError(err).Debug("fatal error building exit scripts") + state = models.ContainerStateDead + } else { + if err = h.Run(); err != nil { + l.WithError(err).Debug("fatal error running exit scripts") + state = models.ContainerStateDead + } + } + // process is over, set the state c.mutex.Lock() defer c.mutex.Unlock() diff --git a/internal/api/container_hook.go b/internal/api/container_hook.go new file mode 100644 index 0000000..115dc3e --- /dev/null +++ b/internal/api/container_hook.go @@ -0,0 +1,191 @@ +package api + +import ( + "bytes" + "compress/bzip2" + "compress/gzip" + "encoding/base64" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + + "github.com/kraken-hpc/imageapi/models" + "github.com/kraken-hpc/uinit" +) + +// Script wraps uinit.Script with running logic for container hooks +type Script struct { + Script *uinit.Script + Must bool +} + +// NewScript builds a new script based on script, mountpoint, log, and keyvalue store +// Note: we need mountpoint so mountpoint-relative paths can be resolved +func NewScript(script *models.ContainerScript, mountpoint string, ctx *uinit.ModuleContext) (*Script, error) { + s := &Script{} + if err := s.Build(script, mountpoint, ctx); err != nil { + return nil, err + } + return s, nil +} + +// Run the script; only return an error if Must +func (s *Script) Run(ctx *uinit.ModuleContext) error { + s.Script.Context = ctx + err := s.Script.Run() + if err != nil && s.Must { + return err + } + return nil +} + +// Build initializes a Script based on a ContainerScript specification +func (s *Script) Build(script *models.ContainerScript, mountpoint string, ctx *uinit.ModuleContext) error { + var err error + var data []byte + var ns *uinit.Script + s.Must = script.Must != nil && *script.Must + switch script.Encoding { + case models.ContainerScriptEncodingFile: + if data, err = ioutil.ReadFile(script.Script); err != nil { + err = fmt.Errorf("failed to read script file %s: %v", script.Script, err) + goto error + } + case models.ContainerScriptEncodingContainerFile: + if data, err = ioutil.ReadFile(filepath.Join(mountpoint, script.Script)); err != nil { + err = fmt.Errorf("failed to read script file %s: %v", filepath.Join(mountpoint, script.Script), err) + goto error + } + case models.ContainerScriptEncodingPlain: + data = ([]byte)(script.Script) + case models.ContainerScriptEncodingBase64: + if data, err = base64.StdEncoding.DecodeString(script.Script); err != nil { + err = fmt.Errorf("failed to decode base64 script: %v", err) + goto error + } + case models.ContainerScriptEncodingGzip: + if data, err = base64.StdEncoding.DecodeString(script.Script); err != nil { + err = fmt.Errorf("failed to decode base64/gzip script: %v", err) + goto error + } + var r *gzip.Reader + if r, err = gzip.NewReader(bytes.NewReader(data)); err != nil { + err = fmt.Errorf("could not decompress gzip script: %v", err) + goto error + } + if data, err = ioutil.ReadAll(r); err != nil { + err = fmt.Errorf("could not decompress gzip script: %v", err) + goto error + } + case models.ContainerScriptEncodingBzip2: + if data, err = base64.StdEncoding.DecodeString(script.Script); err != nil { + err = fmt.Errorf("failed to decode base64/gzip script: %v", err) + goto error + } + r := bzip2.NewReader(bytes.NewReader(data)) + if data, err = ioutil.ReadAll(r); err != nil { + err = fmt.Errorf("failed to decode base64/bzip2 script: %v", err) + goto error + } + default: + return fmt.Errorf("unrecognized encoding type: %v", script.Encoding) + } + if ns, err = uinit.NewScript(data, nil); err != nil { + err = fmt.Errorf("failed to parse script: %v", err) + goto error + } + s.Script = ns + s.Script.Context = ctx + return nil + +error: + // we set these even if it's not fatal + script.Success = new(bool) + script.LastError = err.Error() + if s.Must { + return err + } + ctx.Log.Printf("failed to parse non-mandatory script: %v", err) + return nil +} + +// A Hook is a sequence of scripts, includes error handling logic for running +type Hook struct { + Scripts []*Script + Context *uinit.ModuleContext + Mountpoint string + DefaultScript string +} + +// NewHook creates an initialized, built hook +func NewHook(csh *models.ContainerScriptHook, defaultScript, mountpoint string, l *log.Logger, vars map[string]string) (hook *Hook, err error) { + hook = &Hook{ + Scripts: []*Script{}, + Context: &uinit.ModuleContext{ + Vars: uinit.NewSimpleKV(), + Log: l, + }, + Mountpoint: mountpoint, + DefaultScript: defaultScript, + } + if hook.Context.Log == nil { + hook.Context.Log = log.New(os.Stdout, "", 0) + } + if hook.Context.Vars == nil { + hook.Context.Vars = uinit.NewSimpleKV() + } + for k, v := range vars { + hook.Context.Vars.Set(k, v) + } + if csh != nil { + if err = hook.Build(csh); err != nil { + return nil, err + } + } + return +} + +func (h *Hook) Run() error { + for _, script := range h.Scripts { + if err := script.Run(h.Context); err != nil { + return fmt.Errorf("mandatory script failed: %v", err) + } + } + return nil +} + +func (h *Hook) Build(hook *models.ContainerScriptHook) (err error) { + h.Scripts = []*Script{} + if hook == nil { + hook = &models.ContainerScriptHook{ + Scripts: []*models.ContainerScript{}, + DisableDefaults: new(bool), + } + } + if (hook.DisableDefaults == nil || !*hook.DisableDefaults) && h.DefaultScript != "" { + var s *Script + if s, _ = NewScript(&models.ContainerScript{ + Encoding: models.ContainerScriptEncodingFile, + Script: h.DefaultScript, + Must: new(bool), // should this be hard-coded? + }, h.Mountpoint, h.Context); s.Script != nil { + h.Scripts = append(h.Scripts, s) + } else { + h.Context.Log.Printf("not adding default script, non-fatal error") + } + } + for i, script := range hook.Scripts { + var s *Script + if s, err = NewScript(script, h.Mountpoint, h.Context); err != nil { + return err + } + if s.Script != nil { // this happens if a non-fatal error occurred + h.Scripts = append(h.Scripts, s) + } else { + h.Context.Log.Printf("not adding script %d, non-fatal error", i) + } + } + return +} diff --git a/models/container.go b/models/container.go index 618373b..1fdc7d7 100644 --- a/models/container.go +++ b/models/container.go @@ -30,6 +30,9 @@ type Container struct { // Required: true Command *string `json:"command"` + // hooks + Hooks *ContainerScriptHooks `json:"hooks,omitempty"` + // id ID ID `json:"id,omitempty"` @@ -72,6 +75,10 @@ func (m *Container) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateHooks(formats); err != nil { + res = append(res, err) + } + if err := m.validateID(formats); err != nil { res = append(res, err) } @@ -107,6 +114,23 @@ func (m *Container) validateCommand(formats strfmt.Registry) error { return nil } +func (m *Container) validateHooks(formats strfmt.Registry) error { + if swag.IsZero(m.Hooks) { // not required + return nil + } + + if m.Hooks != nil { + if err := m.Hooks.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("hooks") + } + return err + } + } + + return nil +} + func (m *Container) validateID(formats strfmt.Registry) error { if swag.IsZero(m.ID) { // not required return nil @@ -193,6 +217,10 @@ func (m *Container) validateState(formats strfmt.Registry) error { func (m *Container) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error + if err := m.contextValidateHooks(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateID(ctx, formats); err != nil { res = append(res, err) } @@ -227,6 +255,20 @@ func (m *Container) ContextValidate(ctx context.Context, formats strfmt.Registry return nil } +func (m *Container) contextValidateHooks(ctx context.Context, formats strfmt.Registry) error { + + if m.Hooks != nil { + if err := m.Hooks.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("hooks") + } + return err + } + } + + return nil +} + func (m *Container) contextValidateID(ctx context.Context, formats strfmt.Registry) error { if err := m.ID.ContextValidate(ctx, formats); err != nil { diff --git a/models/container_script.go b/models/container_script.go new file mode 100644 index 0000000..7123018 --- /dev/null +++ b/models/container_script.go @@ -0,0 +1,171 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ContainerScript A `uinit` style script to be executed on container load/unload. +// +// Scripts can be passed in several ways, as specified by `encoding`: +// - `file` : `script` must be a path to a valid file on the root filesystem. +// - `container_file` : `script` must be a path to a valid file in the contianer filesystem. +// - `plain` : multi-line script string. +// - `base64` : base64 encoded script string. +// - `gzip` : gzip + base64 encoded script string. +// - `bzip2` : bzip2 + base64 encoded script string. +// +// +// swagger:model container_script +type ContainerScript struct { + + // The type of script specification contained in `script` + // Enum: [file container_file plain base64 gzip bzip2] + Encoding string `json:"encoding,omitempty"` + + // The last error message reported by this script + // Read Only: true + LastError string `json:"last_error,omitempty"` + + // Any script failure is considered fatal + Must *bool `json:"must,omitempty"` + + // String either containing the script, or a script file location + Script string `json:"script,omitempty"` + + // Was the last run of this script successful + // Read Only: true + Success *bool `json:"success,omitempty"` +} + +// Validate validates this container script +func (m *ContainerScript) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateEncoding(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +var containerScriptTypeEncodingPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["file","container_file","plain","base64","gzip","bzip2"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + containerScriptTypeEncodingPropEnum = append(containerScriptTypeEncodingPropEnum, v) + } +} + +const ( + + // ContainerScriptEncodingFile captures enum value "file" + ContainerScriptEncodingFile string = "file" + + // ContainerScriptEncodingContainerFile captures enum value "container_file" + ContainerScriptEncodingContainerFile string = "container_file" + + // ContainerScriptEncodingPlain captures enum value "plain" + ContainerScriptEncodingPlain string = "plain" + + // ContainerScriptEncodingBase64 captures enum value "base64" + ContainerScriptEncodingBase64 string = "base64" + + // ContainerScriptEncodingGzip captures enum value "gzip" + ContainerScriptEncodingGzip string = "gzip" + + // ContainerScriptEncodingBzip2 captures enum value "bzip2" + ContainerScriptEncodingBzip2 string = "bzip2" +) + +// prop value enum +func (m *ContainerScript) validateEncodingEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, containerScriptTypeEncodingPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *ContainerScript) validateEncoding(formats strfmt.Registry) error { + if swag.IsZero(m.Encoding) { // not required + return nil + } + + // value enum + if err := m.validateEncodingEnum("encoding", "body", m.Encoding); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this container script based on the context it is used +func (m *ContainerScript) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateLastError(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateSuccess(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerScript) contextValidateLastError(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "last_error", "body", string(m.LastError)); err != nil { + return err + } + + return nil +} + +func (m *ContainerScript) contextValidateSuccess(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "success", "body", m.Success); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ContainerScript) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ContainerScript) UnmarshalBinary(b []byte) error { + var res ContainerScript + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/container_script_hook.go b/models/container_script_hook.go new file mode 100644 index 0000000..d27f32d --- /dev/null +++ b/models/container_script_hook.go @@ -0,0 +1,118 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// ContainerScriptHook Describes a container script hook point with execution controls. +// +// Scripts will be executed in array order after any default scripts. +// +// +// swagger:model container_script_hook +type ContainerScriptHook struct { + + // Disable default script hooks. + DisableDefaults *bool `json:"disable_defaults,omitempty"` + + // scripts + Scripts []*ContainerScript `json:"scripts"` +} + +// Validate validates this container script hook +func (m *ContainerScriptHook) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateScripts(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerScriptHook) validateScripts(formats strfmt.Registry) error { + if swag.IsZero(m.Scripts) { // not required + return nil + } + + for i := 0; i < len(m.Scripts); i++ { + if swag.IsZero(m.Scripts[i]) { // not required + continue + } + + if m.Scripts[i] != nil { + if err := m.Scripts[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("scripts" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this container script hook based on the context it is used +func (m *ContainerScriptHook) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateScripts(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerScriptHook) contextValidateScripts(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Scripts); i++ { + + if m.Scripts[i] != nil { + if err := m.Scripts[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("scripts" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ContainerScriptHook) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ContainerScriptHook) UnmarshalBinary(b []byte) error { + var res ContainerScriptHook + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/container_script_hooks.go b/models/container_script_hooks.go new file mode 100644 index 0000000..026dd5e --- /dev/null +++ b/models/container_script_hooks.go @@ -0,0 +1,233 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// ContainerScriptHooks Container script execution hooks. +// +// We currently provide 4 hook points: +// 1. `create` is executed on container creation (root namespaces). +// 2. `init` is executed in the container namespaces before the provided `init` is called (container namespaces). +// 3. `exit` is executed on container exit (root namespaces). +// 4. `delete` is executed on container deletion (root namespaces). +// +// +// swagger:model container_script_hooks +type ContainerScriptHooks struct { + + // create + Create *ContainerScriptHook `json:"create,omitempty"` + + // delete + Delete *ContainerScriptHook `json:"delete,omitempty"` + + // exit + Exit *ContainerScriptHook `json:"exit,omitempty"` + + // init + Init *ContainerScriptHook `json:"init,omitempty"` +} + +// Validate validates this container script hooks +func (m *ContainerScriptHooks) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateCreate(formats); err != nil { + res = append(res, err) + } + + if err := m.validateDelete(formats); err != nil { + res = append(res, err) + } + + if err := m.validateExit(formats); err != nil { + res = append(res, err) + } + + if err := m.validateInit(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerScriptHooks) validateCreate(formats strfmt.Registry) error { + if swag.IsZero(m.Create) { // not required + return nil + } + + if m.Create != nil { + if err := m.Create.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("create") + } + return err + } + } + + return nil +} + +func (m *ContainerScriptHooks) validateDelete(formats strfmt.Registry) error { + if swag.IsZero(m.Delete) { // not required + return nil + } + + if m.Delete != nil { + if err := m.Delete.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("delete") + } + return err + } + } + + return nil +} + +func (m *ContainerScriptHooks) validateExit(formats strfmt.Registry) error { + if swag.IsZero(m.Exit) { // not required + return nil + } + + if m.Exit != nil { + if err := m.Exit.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("exit") + } + return err + } + } + + return nil +} + +func (m *ContainerScriptHooks) validateInit(formats strfmt.Registry) error { + if swag.IsZero(m.Init) { // not required + return nil + } + + if m.Init != nil { + if err := m.Init.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("init") + } + return err + } + } + + return nil +} + +// ContextValidate validate this container script hooks based on the context it is used +func (m *ContainerScriptHooks) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateCreate(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateDelete(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateExit(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateInit(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerScriptHooks) contextValidateCreate(ctx context.Context, formats strfmt.Registry) error { + + if m.Create != nil { + if err := m.Create.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("create") + } + return err + } + } + + return nil +} + +func (m *ContainerScriptHooks) contextValidateDelete(ctx context.Context, formats strfmt.Registry) error { + + if m.Delete != nil { + if err := m.Delete.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("delete") + } + return err + } + } + + return nil +} + +func (m *ContainerScriptHooks) contextValidateExit(ctx context.Context, formats strfmt.Registry) error { + + if m.Exit != nil { + if err := m.Exit.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("exit") + } + return err + } + } + + return nil +} + +func (m *ContainerScriptHooks) contextValidateInit(ctx context.Context, formats strfmt.Registry) error { + + if m.Init != nil { + if err := m.Init.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("init") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ContainerScriptHooks) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ContainerScriptHooks) UnmarshalBinary(b []byte) error { + var res ContainerScriptHooks + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/restapi/doc.go b/restapi/doc.go index 4305d9b..9121da4 100644 --- a/restapi/doc.go +++ b/restapi/doc.go @@ -19,7 +19,7 @@ // https // Host: localhost // BasePath: /imageapi/v1 -// Version: 0.2.0 +// Version: 0.2.1 // // Consumes: // - application/json diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 412afd5..b8f9f43 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -32,7 +32,7 @@ func init() { "info": { "description": "This API specification describes a service for attaching, mounting and preparing container images and manipulating those containers.\n\nIn general, higher level objects can either reference lower level objects (e.g. a mount referencing an attachment point) by a reference ID, \nor, they can contain the full specification of those lower objects.\n\nIf an object references another by ID, deletion of that object does not effect the underlying object.\n\nIf an object defines a lower level object, that lower level object will automatically be deleted on deletion of the higher level object.\n\nFor instance, if a container contains all of the defintions for all mount points and attachments, deletion of the container will automatically unmount\nand detach those lower objects.\n", "title": "Image API", - "version": "0.2.0" + "version": "0.2.1" }, "basePath": "/imageapi/v1", "paths": { @@ -630,6 +630,9 @@ func init() { "command": { "type": "string" }, + "hooks": { + "$ref": "#/definitions/container_script_hooks" + }, "id": { "$ref": "#/definitions/id" }, @@ -680,6 +683,78 @@ func init() { "uts" ] }, + "container_script": { + "description": "A ` + "`" + `uinit` + "`" + ` style script to be executed on container load/unload.\n\nScripts can be passed in several ways, as specified by ` + "`" + `encoding` + "`" + `:\n- ` + "`" + `file` + "`" + ` : ` + "`" + `script` + "`" + ` must be a path to a valid file on the root filesystem.\n- ` + "`" + `container_file` + "`" + ` : ` + "`" + `script` + "`" + ` must be a path to a valid file in the contianer filesystem.\n- ` + "`" + `plain` + "`" + ` : multi-line script string.\n- ` + "`" + `base64` + "`" + ` : base64 encoded script string.\n- ` + "`" + `gzip` + "`" + ` : gzip + base64 encoded script string.\n- ` + "`" + `bzip2` + "`" + ` : bzip2 + base64 encoded script string.\n", + "type": "object", + "properties": { + "encoding": { + "description": "The type of script specification contained in ` + "`" + `script` + "`" + `", + "type": "string", + "enum": [ + "file", + "container_file", + "plain", + "base64", + "gzip", + "bzip2" + ] + }, + "last_error": { + "description": "The last error message reported by this script", + "type": "string", + "readOnly": true + }, + "must": { + "description": "Any script failure is considered fatal", + "type": "boolean", + "default": false + }, + "script": { + "description": "String either containing the script, or a script file location", + "type": "string" + }, + "success": { + "description": "Was the last run of this script successful", + "type": "boolean", + "readOnly": true + } + } + }, + "container_script_hook": { + "description": "Describes a container script hook point with execution controls.\n\nScripts will be executed in array order after any default scripts.\n", + "type": "object", + "properties": { + "disable_defaults": { + "description": "Disable default script hooks.", + "type": "boolean", + "default": false + }, + "scripts": { + "type": "array", + "items": { + "$ref": "#/definitions/container_script" + } + } + } + }, + "container_script_hooks": { + "description": "Container script execution hooks.\n\nWe currently provide 4 hook points:\n1. ` + "`" + `create` + "`" + ` is executed on container creation (root namespaces).\n2. ` + "`" + `init` + "`" + ` is executed in the container namespaces before the provided ` + "`" + `init` + "`" + ` is called (container namespaces). \n3. ` + "`" + `exit` + "`" + ` is executed on container exit (root namespaces).\n4. ` + "`" + `delete` + "`" + ` is executed on container deletion (root namespaces).\n", + "type": "object", + "properties": { + "create": { + "$ref": "#/definitions/container_script_hook" + }, + "delete": { + "$ref": "#/definitions/container_script_hook" + }, + "exit": { + "$ref": "#/definitions/container_script_hook" + }, + "init": { + "$ref": "#/definitions/container_script_hook" + } + } + }, "container_state": { "description": "Valid container states", "type": "string", @@ -983,7 +1058,7 @@ func init() { "info": { "description": "This API specification describes a service for attaching, mounting and preparing container images and manipulating those containers.\n\nIn general, higher level objects can either reference lower level objects (e.g. a mount referencing an attachment point) by a reference ID, \nor, they can contain the full specification of those lower objects.\n\nIf an object references another by ID, deletion of that object does not effect the underlying object.\n\nIf an object defines a lower level object, that lower level object will automatically be deleted on deletion of the higher level object.\n\nFor instance, if a container contains all of the defintions for all mount points and attachments, deletion of the container will automatically unmount\nand detach those lower objects.\n", "title": "Image API", - "version": "0.2.0" + "version": "0.2.1" }, "basePath": "/imageapi/v1", "paths": { @@ -1581,6 +1656,9 @@ func init() { "command": { "type": "string" }, + "hooks": { + "$ref": "#/definitions/container_script_hooks" + }, "id": { "$ref": "#/definitions/id" }, @@ -1631,6 +1709,78 @@ func init() { "uts" ] }, + "container_script": { + "description": "A ` + "`" + `uinit` + "`" + ` style script to be executed on container load/unload.\n\nScripts can be passed in several ways, as specified by ` + "`" + `encoding` + "`" + `:\n- ` + "`" + `file` + "`" + ` : ` + "`" + `script` + "`" + ` must be a path to a valid file on the root filesystem.\n- ` + "`" + `container_file` + "`" + ` : ` + "`" + `script` + "`" + ` must be a path to a valid file in the contianer filesystem.\n- ` + "`" + `plain` + "`" + ` : multi-line script string.\n- ` + "`" + `base64` + "`" + ` : base64 encoded script string.\n- ` + "`" + `gzip` + "`" + ` : gzip + base64 encoded script string.\n- ` + "`" + `bzip2` + "`" + ` : bzip2 + base64 encoded script string.\n", + "type": "object", + "properties": { + "encoding": { + "description": "The type of script specification contained in ` + "`" + `script` + "`" + `", + "type": "string", + "enum": [ + "file", + "container_file", + "plain", + "base64", + "gzip", + "bzip2" + ] + }, + "last_error": { + "description": "The last error message reported by this script", + "type": "string", + "readOnly": true + }, + "must": { + "description": "Any script failure is considered fatal", + "type": "boolean", + "default": false + }, + "script": { + "description": "String either containing the script, or a script file location", + "type": "string" + }, + "success": { + "description": "Was the last run of this script successful", + "type": "boolean", + "readOnly": true + } + } + }, + "container_script_hook": { + "description": "Describes a container script hook point with execution controls.\n\nScripts will be executed in array order after any default scripts.\n", + "type": "object", + "properties": { + "disable_defaults": { + "description": "Disable default script hooks.", + "type": "boolean", + "default": false + }, + "scripts": { + "type": "array", + "items": { + "$ref": "#/definitions/container_script" + } + } + } + }, + "container_script_hooks": { + "description": "Container script execution hooks.\n\nWe currently provide 4 hook points:\n1. ` + "`" + `create` + "`" + ` is executed on container creation (root namespaces).\n2. ` + "`" + `init` + "`" + ` is executed in the container namespaces before the provided ` + "`" + `init` + "`" + ` is called (container namespaces). \n3. ` + "`" + `exit` + "`" + ` is executed on container exit (root namespaces).\n4. ` + "`" + `delete` + "`" + ` is executed on container deletion (root namespaces).\n", + "type": "object", + "properties": { + "create": { + "$ref": "#/definitions/container_script_hook" + }, + "delete": { + "$ref": "#/definitions/container_script_hook" + }, + "exit": { + "$ref": "#/definitions/container_script_hook" + }, + "init": { + "$ref": "#/definitions/container_script_hook" + } + } + }, "container_state": { "description": "Valid container states", "type": "string", diff --git a/swagger.yaml b/swagger.yaml index 29fd527..bcba076 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,33 +1,38 @@ --- swagger: "2.0" consumes: -- application/json + - application/json info: description: | - This API specification describes a service for attaching, mounting and preparing container images and manipulating those containers. + This API specification describes a service for attaching, mounting and + preparing container images and manipulating those containers. - In general, higher level objects can either reference lower level objects (e.g. a mount referencing an attachment point) by a reference ID, + In general, higher level objects can either reference lower level objects + (e.g. a mount referencing an attachment point) by a reference ID, or, they can contain the full specification of those lower objects. - If an object references another by ID, deletion of that object does not effect the underlying object. - - If an object defines a lower level object, that lower level object will automatically be deleted on deletion of the higher level object. + If an object references another by ID, deletion of that object does not + effect the underlying object. - For instance, if a container contains all of the defintions for all mount points and attachments, deletion of the container will automatically unmount + If an object defines a lower level object, that lower level object will + automatically be deleted on deletion of the higher level object. + + For instance, if a container contains all of the defintions for all mount + points and attachments, deletion of the container will automatically unmount and detach those lower objects. title: Image API - version: 0.2.0 + version: 0.2.1 produces: -- application/json + - application/json schemes: -- http -- https + - http + - https basePath: /imageapi/v1 definitions: id: description: | - An ID is a unique numeric ID that references an object. + An ID is a unique numeric ID that references an object. IDs are not necessarily unique across object types. IDs are generall readOnly and generated internally. type: integer @@ -37,7 +42,8 @@ definitions: description: | A name is a unique, user-provided identifier for an object. - A name must consist of numbers, letters, and the symbols in the set { `.`, `-`, `_`}. + A name must consist of numbers, letters, and the symbols in the set + { `.`, `-`, `_`}. type: string pattern: "^[A-Za-z0-1.\\-_]*$" @@ -106,8 +112,9 @@ definitions: attach_rbd: description: | - attach_rbd describes an RBD map. To successfully map, at least one monitor, pool and image must be specified. - Additionally, you will need options.name and options.secret specified. + attach_rbd describes an RBD map. To successfully map, at least one + monitor, pool and image must be specified. Additionally, you will need + options.name and options.secret specified. type: object required: - monitors @@ -119,7 +126,7 @@ definitions: type: integer format: int64 readOnly: true - #external + # external monitors: type: array items: @@ -136,7 +143,7 @@ definitions: type: string options: $ref: "#/definitions/rbd_options" - + attach_loopback: description: | `attach_loopback` describes a loopback device based on an available file. @@ -149,14 +156,18 @@ definitions: properties: path: type: string - description: A unix-formatted filesystem path with `/` relative to the respective base. + description: > + A unix-formatted filesystem path with `/` relative to the respective + base. base: - description: | - base determines the relative root for the path. There are two options: + description: > + base determines the relative root for the path. There are two + options: `root` means to use the current root (`/`) as the base path. - `mount` means to use a mount as the base path. If this is specified, `mount` must be specified as well. + `mount` means to use a mount as the base path. If this is specified, + `mount` must be specified as well. type: string - enum: [ "root", "mount" ] + enum: ["root", "mount"] readPartitions: description: | Should the partition table on the looback device be read? @@ -179,8 +190,9 @@ definitions: properties: path: type: string - description: A unix-formatted filesystem path pointing to a block device file. - + description: > + A unix-formatted filesystem path pointing to a block device file. + attach_iscsi: description: | `attach_iscsi` describes an block device available as an iSCSI attachment. @@ -229,22 +241,25 @@ definitions: description: Block device scheduler attach: - description: | - Generically address attachments. Attachments are objects that ultimately provide a block device file. + description: > + Generically address attachments. Attachments are objects that ultimately + provide a block device file. properties: id: $ref: "#/definitions/id" readOnly: true kind: type: string - enum: [ "iscsi", "local", "loopback", "rbd" ] + enum: ["iscsi", "local", "loopback", "rbd"] description: | - Kind specifies the kind of attachment. Each kind has corresponding kind-specific options. + Kind specifies the kind of attachment. Each kind has corresponding + kind-specific options. Currently known kinds: iscsi - attach an iscsi lun - local - create an attachment reference to an existing block device (specifying a non-block device will fail) + local - create an attachment reference to an existing block device + (specifying a non-block device will fail) loopback - create a loopback device referencing a file in a mount rbd - attach a Ceph/RBD object @@ -268,13 +283,14 @@ definitions: mount_attach: description: | - `mount_attach` describes an attach mount. This must have at least attach ID associated with it, - and a provided filesystem type. - - Either `attach_id` or `attach` must be specified. If both are specified, `attach` will be ignored. + `mount_attach` describes an attach mount. This must have at least attach + ID associated with it, and a provided filesystem type. + + Either `attach_id` or `attach` must be specified. If both are specified, + `attach` will be ignored. - If `attach` is specified and `attach_id` is omitted, the specified attach will first be attached, and will be - detached on deletion. + If `attach` is specified and `attach_id` is omitted, the specified attach + will first be attached, and will be detached on deletion. required: - fs_type - attach @@ -284,19 +300,22 @@ definitions: fs_type: type: string mount_options: - description: these mount options will be passed to the mount syscall. Supported options depend on filesystem type. + description: > + these mount options will be passed to the mount syscall. Supported + options depend on filesystem type. type: array items: type: string mount_overlay: description: | - `mount_overlay` describes an Overlayfs mount. All mount points must be RBD ID's. - At very least, `lower` must be specified. If `upper` length is zero, no `upper` - mounts will be used. `workdir` will be assigned automatically. + `mount_overlay` describes an Overlayfs mount. All mount points must be + RBD ID's. At very least, `lower` must be specified. If `upper` length + is zero, no `upper` mounts will be used. `workdir` will be assigned + automatically. - If the mounts specified in `lower` are specifications and not ID references, they - will be recursively mounted/attached. + If the mounts specified in `lower` are specifications and not ID + references, they will be recursively mounted/attached. Overlay mounts are identified by their uppermost `lower` ID. type: object @@ -313,15 +332,18 @@ definitions: readOnly: true # external lower: - description: This is an array of mount specifications to be used (in order) as lower mounts for the overlay. + description: > + This is an array of mount specifications to be used (in order) as + lower mounts for the overlay. type: array items: $ref: "#/definitions/mount" mount_bind: description: | - `mount_bind` describes a local bind mount. - Bind mounts can be relative to another mount, or to /, allowing a way to access local data. + `mount_bind` describes a local bind mount. + Bind mounts can be relative to another mount, or to /, allowing a way to + access local data. type: object required: - path @@ -329,14 +351,18 @@ definitions: properties: path: type: string - description: A unix-formatted filesystem path with `/` relative to the respective base. + description: > + A unix-formatted filesystem path with `/` relative to the respective + base. base: - description: | - base determines the relative root for the path. There are two options: + description: | + base determines the relative root for the path. There are two + options: `root` means to use the current root (`/`) as the base path. - `mount` means to use a mount as the base path. If this is specified, `mount` must be specified as well. + `mount` means to use a mount as the base path. If this is specified, + `mount` must be specified as well. type: string - enum: [ "root", "mount" ] + enum: ["root", "mount"] recursive: description: perform a recursive bind mount type: boolean @@ -347,7 +373,7 @@ definitions: default: false mount: $ref: "#/definitions/mount" - + mount_nfs: description: | `mount_nfs` describes an NFS mount. @@ -372,9 +398,9 @@ definitions: default: "4.2" options: description: | - Options as specified in nfs(5). General mount options won't work here. - addr= and clientaddr= will be filled out automatically based on host. - vers= will be filled by version + Options as specified in nfs(5). General mount options won't work + here. addr= and clientaddr= will be filled out automatically based + on host. vers= will be filled by version type: array items: type: string @@ -384,17 +410,19 @@ definitions: Generically address mounts by kind and ID or definition Either an `mount_id` or a mount definition must be supplied. If both are supplied, the mount definition will be ignored. - If `mount_id` is specified, then the kind/id will be used to reference that mount. - If no `mount_id` is supplied a defition of type `kind` must be present. + If `mount_id` is specified, then the kind/id will be used to reference + that mount. If no `mount_id` is supplied a defition of type `kind` must + be present. type: object properties: id: $ref: "#/definitions/id" kind: type: string - enum: [ "attach", "bind", "nfs", "overlay", "uri" ] + enum: ["attach", "bind", "nfs", "overlay", "uri"] description: | - Kind specifies the kind of mount. Each kind has corresponding kind-specific options. + Kind specifies the kind of mount. Each kind has corresponding + kind-specific options. Currently known kinds: @@ -425,13 +453,85 @@ definitions: container_namespace: description: Linux namespace type: string - enum: [ "cgroup", "ipc", "net", "mnt", "pid", "time", "user", "uts" ] + enum: ["cgroup", "ipc", "net", "mnt", "pid", "time", "user", "uts"] container_state: description: Valid container states type: string # stolen straight from docker (even if we don't use them all) - enum: [ "created", "running", "stopping", "exited", "dead" ] + enum: ["created", "running", "stopping", "exited", "dead"] + + container_script: + description: | + A `uinit` style script to be executed on container load/unload. + + Scripts can be passed in several ways, as specified by `encoding`: + - `file` : `script` must be a path to a valid file on the root filesystem. + - `container_file` : `script` must be a path to a valid file in the + contianer filesystem. + - `plain` : multi-line script string. + - `base64` : base64 encoded script string. + - `gzip` : gzip + base64 encoded script string. + - `bzip2` : bzip2 + base64 encoded script string. + type: object + properties: + encoding: + description: The type of script specification contained in `script` + type: string + enum: ["file", "container_file", "plain", "base64", "gzip", "bzip2"] + script: + description: > + String either containing the script, or a script file location + type: string + must: + description: Any script failure is considered fatal + type: boolean + default: false + success: + description: Was the last run of this script successful + type: boolean + readOnly: true + last_error: + description: The last error message reported by this script + type: string + readOnly: true + + container_script_hook: + description: | + Describes a container script hook point with execution controls. + + Scripts will be executed in array order after any default scripts. + type: object + properties: + scripts: + type: array + items: + $ref: "#/definitions/container_script" + disable_defaults: + description: Disable default script hooks. + type: boolean + default: false + + container_script_hooks: + description: | + Container script execution hooks. + + We currently provide 4 hook points: + 1. `create` is executed on container creation (root namespaces). + 2. `init` is executed in the container namespaces before the provided + `init` is called (container namespaces). + 3. `exit` is executed on container exit (root namespaces). + 4. `delete` is executed on container deletion (root namespaces). + type: object + properties: + create: + $ref: "#/definitions/container_script_hook" + init: + $ref: "#/definitions/container_script_hook" + exit: + $ref: "#/definitions/container_script_hook" + delete: + $ref: "#/definitions/container_script_hook" container: description: | @@ -454,7 +554,9 @@ definitions: readOnly: true # external name: - description: name is an optional identifier for the container. Name must be unique. + description: > + name is an optional identifier for the container. Name must be + unique. $ref: "#/definitions/name" mount: $ref: "#/definitions/mount" @@ -463,29 +565,32 @@ definitions: systemd: type: boolean description: > - When `systemd` is set to `true`, we will assume that this container will run `systemd`, - and perform the necessary magic dance to make systemd run inside of the container. - The default is `false`. + When `systemd` is set to `true`, we will assume that this container + will run `systemd`, and perform the necessary magic dance to make + systemd run inside of the container. The default is `false`. state: description: > When read, this contains the current container state. - On creation, this requests the initial state (valid options: `created` or `running`). + On creation, this requests the initial state (valid options: + `created` or `running`). The default is `created`. $ref: "#/definitions/container_state" namespaces: description: | A list of Linux namespaces to use. - Note: This is currently unused. All containers currently get `mnt` and `pid`. - It's here as a placeholder for future use. + Note: This is currently unused. All containers currently get `mnt` + and `pid`. It's here as a placeholder for future use. type: array items: $ref: "#/definitions/container_namespace" + hooks: + $ref: "#/definitions/container_script_hooks" refs: type: integer format: int64 readOnly: true - + error: type: object required: @@ -497,7 +602,7 @@ definitions: message: type: string - + paths: /attach: @@ -513,7 +618,7 @@ paths: - in: query name: kind type: string - enum: [ "iscsi", "local", "loopback", "rbd" ] + enum: ["iscsi", "local", "loopback", "rbd"] required: false description: Kind of attachments to query. tags: @@ -588,7 +693,7 @@ paths: - in: query name: kind type: string - enum: [ "attach", "bind", "nfs", "overlay", "uri" ] + enum: ["attach", "bind", "nfs", "overlay", "uri"] required: false description: Kind of mounts to query. description: List mounts @@ -627,7 +732,9 @@ paths: schema: $ref: "#/definitions/error" delete: - description: Unmount a specified mount. Note that mount reference IDs must be specified. + description: | + Unmount a specified mount. Note that mount reference IDs must be + specified. tags: - mounts parameters: @@ -652,7 +759,7 @@ paths: description: Unmount failed schema: $ref: "#/definitions/error" - + /container: get: parameters: @@ -670,7 +777,7 @@ paths: - in: query name: state type: string - enum: [ "created", "running", "stopping", "exited", "dead" ] + enum: ["created", "running", "stopping", "exited", "dead"] required: false description: Query containers by state tags: @@ -731,7 +838,7 @@ paths: tags: - containers description: | - Delete a container defition. + Delete a container defition. Either `id` or `name` query parameter must be specified. operationId: delete_container responses: @@ -750,7 +857,7 @@ paths: - in: query name: state type: string - enum: [ "running", "exited", "paused" ] + enum: ["running", "exited", "paused"] required: true description: Desired container state - in: query @@ -765,10 +872,12 @@ paths: required: false description: Name of container description: | - Request a (valid) state for a container. - Valid states to request include: `running`, `exited`, `paused` (paused is not yet implemented) + Request a (valid) state for a container. + Valid states to request include: `running`, `exited`, `paused` (paused + is not yet implemented) - Either a valid Name or ID must be passed as a query parameter, along with a valid state parameter. + Either a valid Name or ID must be passed as a query parameter, along + with a valid state parameter. operationId: set_container_state responses: 200: @@ -778,4 +887,4 @@ paths: default: description: error schema: - $ref: "#/definitions/error" \ No newline at end of file + $ref: "#/definitions/error"