diff --git a/cmd/util.go b/cmd/util.go deleted file mode 100644 index fd041e0..0000000 --- a/cmd/util.go +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 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 cmd - -import ( - "bytes" - "encoding/json" - "fmt" - "time" - - "github.com/aurora-scheduler/gorealis/v2/gen-go/apache/aurora" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -type monitorCmdConfig struct { - cmd *cobra.Command - monitorInterval, monitorTimeout time.Duration - statusList []string -} - -func toJSON(v interface{}) string { - - output, err := json.Marshal(v) - - if err != nil { - log.Fatalln("Unable to serialize Aurora response: %+v", v) - } - - return string(output) -} - -func getLoggingLevels() string { - - var buffer bytes.Buffer - - for _, level := range logrus.AllLevels { - buffer.WriteString(level.String()) - buffer.WriteString(" ") - } - - buffer.Truncate(buffer.Len() - 1) - - return buffer.String() - -} - -func maintenanceMonitorPrint(hostResult map[string]bool, desiredStates []aurora.MaintenanceMode) { - if len(hostResult) > 0 { - // Create anonymous struct for JSON formatting - output := struct { - DesiredStates []string `json:desired_states` - Transitioned []string `json:transitioned` - NonTransitioned []string `json:non-transitioned` - }{ - make([]string, 0), - make([]string, 0), - make([]string, 0), - } - - for _, state := range desiredStates { - output.DesiredStates = append(output.DesiredStates, state.String()) - } - - for host, ok := range hostResult { - if ok { - output.Transitioned = append(output.Transitioned, host) - } else { - output.NonTransitioned = append(output.NonTransitioned, host) - } - } - - if toJson { - fmt.Println(toJSON(output)) - } else { - fmt.Printf("Entered %v status: %v\n", output.DesiredStates, output.Transitioned) - fmt.Printf("Did not enter %v status: %v\n", output.DesiredStates, output.NonTransitioned) - } - } -} diff --git a/go.mod b/go.mod index a608f6f..ffd3e03 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.6.3 github.com/stretchr/objx v0.1.1 // indirect + github.com/stretchr/testify v1.5.0 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/internal/types.go b/internal/types.go new file mode 100644 index 0000000..6c77dff --- /dev/null +++ b/internal/types.go @@ -0,0 +1,85 @@ +package internal + +import ( + "time" +) + +type URI struct { + URI string `yaml:"uri"` + Extract bool `yaml:"extract"` + Cache bool `yaml:"cache"` +} + +type Executor struct { + Name string `yaml:"name"` + Data string `yaml:"data"` +} + +type ThermosProcess struct { + Name string `yaml:"name"` + Cmd string `yaml:"cmd"` +} + +type DockerContainer struct { + Name string `yaml:"name"` + Tag string `yaml:"tag"` +} + +type Container struct { + Docker *DockerContainer `yaml:"docker"` +} + +type Job struct { + Environment string `yaml:"environment"` + Role string `yaml:"role"` + Name string `yaml:"name"` + CPU float64 `yaml:"cpu"` + RAM int64 `yaml:"ram"` + Disk int64 `yaml:"disk"` + Executor Executor `yaml:"executor"` + Instances int32 `yaml:"instances"` + URIs []URI `yaml:"uris"` + Metadata map[string]string `yaml:"labels"` + Service bool `yaml:"service"` + Thermos []ThermosProcess `yaml:",flow,omitempty"` + Container *Container `yaml:"container,omitempty"` +} +type InstanceRange struct { + Start int `yaml:"start"` + End int `yaml:"end"` +} + +type VariableBatchStrategy struct { + GroupSizes []int `yaml:"groupSizes"` + AutoPause bool `yaml:"autoPause"` +} + +type BatchStrategy struct { + GroupSize int `yaml:"groupSize"` + AutoPause bool `yaml:"autoPause"` +} + +type QueueStrategy struct { + GroupSize int `yaml:"groupSize"` +} + +type UpdateStrategy struct { + VariableBatch *VariableBatchStrategy `yaml:"variableBatch"` + Batch *BatchStrategy `yaml:"batch"` + Queue *QueueStrategy `yaml:"queue"` +} +type UpdateSettings struct { + MaxPerInstanceFailures int `yaml:"maxPerInstanceFailures"` + MaxFailedInstances int `yaml:"maxFailedInstances"` + MinTimeInRunning time.Duration `yaml:"minTimeRunning"` + RollbackOnFailure bool `yaml:"rollbackOnFailure"` + InstanceRanges []InstanceRange `yaml:"instanceRanges"` + BlockIfNoPulseAfter time.Duration `yaml:"blockIfNoPulseAfter"` + SLAAware bool `yaml:"slaAware"` + Strategy UpdateStrategy `yaml:"strategy"` +} + +type UpdateJob struct { + JobConfig Job `yaml:"jobConfig"` + UpdateSettings UpdateSettings `yaml:"updateSettings"` +} diff --git a/internal/util.go b/internal/util.go new file mode 100644 index 0000000..9a95be8 --- /dev/null +++ b/internal/util.go @@ -0,0 +1,193 @@ +/** + * 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 internal + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/aurora-scheduler/gorealis/v2/gen-go/apache/aurora" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + yaml "gopkg.in/yaml.v2" +) + +type MonitorCmdConfig struct { + cmd *cobra.Command + monitorInterval, monitorTimeout time.Duration + statusList []string +} + +func toJSON(v interface{}) string { + + output, err := json.Marshal(v) + + if err != nil { + log.Fatalf("Unable to serialize Aurora response: %+v", v) + } + + return string(output) +} + +func getLoggingLevels() string { + + var buffer bytes.Buffer + + for _, level := range logrus.AllLevels { + buffer.WriteString(level.String()) + buffer.WriteString(" ") + } + + buffer.Truncate(buffer.Len() - 1) + + return buffer.String() + +} + +func maintenanceMonitorPrint(hostResult map[string]bool, desiredStates []aurora.MaintenanceMode) { + if len(hostResult) > 0 { + // Create anonymous struct for JSON formatting + output := struct { + DesiredStates []string `json:desired_states` + Transitioned []string `json:transitioned` + NonTransitioned []string `json:non-transitioned` + }{ + make([]string, 0), + make([]string, 0), + make([]string, 0), + } + + for _, state := range desiredStates { + output.DesiredStates = append(output.DesiredStates, state.String()) + } + + for host, ok := range hostResult { + if ok { + output.Transitioned = append(output.Transitioned, host) + } else { + output.NonTransitioned = append(output.NonTransitioned, host) + } + } + + if toJson { + fmt.Println(toJSON(output)) + } else { + fmt.Printf("Entered %v status: %v\n", output.DesiredStates, output.Transitioned) + fmt.Printf("Did not enter %v status: %v\n", output.DesiredStates, output.NonTransitioned) + } + } +} + +func UnmarshalJob(filename string) (Job, error) { + + job := Job{} + + if jobsFile, err := os.Open(filename); err != nil { + return job, errors.Wrap(err, "unable to read the job config file") + } else { + if err := yaml.NewDecoder(jobsFile).Decode(&job); err != nil { + return job, errors.Wrap(err, "unable to parse job config file") + } + + if !job.Validate() { + return job, errors.New("invalid job config") + } + } + + return job, nil +} + +func (j *Job) Validate() bool { + if j.Name == "" { + return false + } + + if j.Role == "" { + return false + } + + if j.Environment == "" { + return false + } + + if j.Instances <= 0 { + return false + } + + if j.CPU <= 0.0 { + return false + } + + if j.RAM <= 0 { + return false + } + + if j.Disk <= 0 { + return false + } + + return true +} + +func UnmarshalUpdate(filename string) (UpdateJob, error) { + + updateJob := UpdateJob{} + + if jobsFile, err := os.Open(filename); err != nil { + return updateJob, errors.Wrap(err, "unable to read the job config file") + } else { + if err := yaml.NewDecoder(jobsFile).Decode(&updateJob); err != nil { + return updateJob, errors.Wrap(err, "unable to parse job config file") + } + + if !updateJob.JobConfig.Validate() { + return updateJob, errors.New("invalid job config") + } + if err := updateJob.UpdateSettings.Validate(); err != nil { + return updateJob, errors.Wrap(err, "invalid update configuration") + } + } + + return updateJob, nil +} + +func (u *UpdateSettings) Validate() error { + if u.Strategy.VariableBatch != nil { + if len(u.Strategy.VariableBatch.GroupSizes) == 0 { + return errors.New("variable batch strategy must specify at least one batch size") + } + for _, batch := range u.Strategy.VariableBatch.GroupSizes { + if batch <= 0 { + return errors.New("all groups in a variable batch strategy must be larger than 0") + } + } + } else if u.Strategy.Batch != nil { + if u.Strategy.Batch.GroupSize <= 0 { + return errors.New("batch strategy must specify a group larger than 0") + } + } else if u.Strategy.Queue != nil { + if u.Strategy.Queue.GroupSize <= 0 { + return errors.New("queue strategy must specify a group larger than 0") + } + } else { + log.Info("No strategy set, falling back on queue strategy with a group size 1") + u.Strategy.Queue = &QueueStrategy{GroupSize: 1} + } + return nil +} diff --git a/internal/util_test.go b/internal/util_test.go new file mode 100644 index 0000000..5610431 --- /dev/null +++ b/internal/util_test.go @@ -0,0 +1,18 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalJob(t *testing.T) { + _, err := UnmarshalJob("../test/hello_world.yaml") + assert.NoError(t, err) +} + +func TestUnmarshalUpdate(t *testing.T) { + _, err := UnmarshalUpdate("../test/update_hello_world.yaml") + assert.NoError(t, err) +} + diff --git a/hello_world.yaml b/test/hello_world.yaml similarity index 51% rename from hello_world.yaml rename to test/hello_world.yaml index 35a76d8..1e04c24 100644 --- a/hello_world.yaml +++ b/test/hello_world.yaml @@ -11,3 +11,16 @@ thermos: cmd: "echo bootstrapping" - name: "hello_gorealis" cmd: "while true; do echo hello world from gorealis; sleep 10; done" +updateSettings: + maxPerInstanceFailures: 1 + maxFailedInstances: 1 + minTimeInRunning: 1m + rollbackOnFailure: true + instanceRanges: + - start: 1 + end: 4 + blockIfNoPulseAfter: 1m + slaAware: false + strategy: + name: Batch + groupSize: 2 \ No newline at end of file