diff --git a/cmd/devopsmastertest/kuber_golden.yaml b/cmd/devopsmastertest/kuber_golden.yaml new file mode 100644 index 0000000..777ddc5 --- /dev/null +++ b/cmd/devopsmastertest/kuber_golden.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Pod +metadata: + name: super_pod + namespace: super_service + labels: + dc: us-west-1 + group: gamma +spec: + os: linux + containers: + - name: my_container_name + image: registry.bigbrother.io/baseimage:v1.2.0 + ports: + - containerPort: 8080 + protocol: TCP + readinessProbe: + httpGet: + path: /_ready + port: 8080 + livenessProbe: + httpGet: + path: /_alive + port: 8080 + resources: + limits: + cpu: 2 + memory: "1Gi" + requests: + cpu: 1 + memory: "500Mi" diff --git a/cmd/devopsmastertest/lesson02_test.go b/cmd/devopsmastertest/lesson02_test.go new file mode 100644 index 0000000..e3f6eca --- /dev/null +++ b/cmd/devopsmastertest/lesson02_test.go @@ -0,0 +1,270 @@ +package main + +import ( + "bytes" + "context" + _ "embed" + "errors" + "fmt" + "math/rand" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/Yandex-Practicum/go-autotests/internal/random" + "github.com/goccy/go-yaml" + yamlast "github.com/goccy/go-yaml/ast" + yamlparser "github.com/goccy/go-yaml/parser" + "github.com/stretchr/testify/suite" +) + +//go:embed kuber_golden.yaml +var goldenYAML []byte + +// Lesson02Suite является сьютом с тестами урока +type Lesson02Suite struct { + suite.Suite +} + +func (suite *Lesson02Suite) TestValidateYAML() { + // проверяем наличие необходимых флагов + suite.Require().NotEmpty(flagTargetBinaryPath, "-binary-path non-empty flag required") + + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + + // сгененрируем новое содержимое YAML файла + suite.T().Log("creating test YAML file") + fpath, modifications, err := newYAMLFile(rnd) + suite.Require().NoError(err, "cannot generate new YAML file content") + + // не забудем удалить за собой временный файл + defer os.Remove(fpath) + + // запускаем бинарник скрипта + suite.T().Log("creating process") + binctx, bincancel := context.WithTimeout(context.Background(), time.Minute) + defer bincancel() + + var scriptOut bytes.Buffer + cmd := exec.CommandContext(binctx, flagTargetBinaryPath, fpath) + cmd.Stdout = &scriptOut + + // ждем завершения скрипта + var exiterr *exec.ExitError + if err := cmd.Run(); errors.As(err, &exiterr) { + suite.Require().NotEqualf(-1, exiterr.ExitCode(), "скрипт завершился аварийно, вывод:\n\n%s", scriptOut.String()) + } + + // соберем и отфильтруем вывод скрипта + linesOut := strings.Split(scriptOut.String(), "\n") + linesOut = slices.DeleteFunc(linesOut, func(line string) bool { + return strings.TrimSpace(line) == "" + }) + + // проверим вывод скрипта + var expectedMessages []string + for _, modification := range modifications { + expectedMessages = append(expectedMessages, modification.message) + } + + matches := suite.Assert().ElementsMatch(expectedMessages, linesOut, "вывод скрипта (List B) не совпадает с ожидаемым (List A)") + if !matches { + content, err := os.ReadFile(fpath) + suite.Require().NoError(err, "невозможно прочитать содержимое YAML файла") + suite.T().Logf("Содержимое тестового YAML файла:\n\n%s\n", content) + } +} + +func newYAMLFile(rnd *rand.Rand) (fpath string, modifications []yamlModification, err error) { + // сгенерируем случайное имя файла и путь + fname := random.ASCIIString(5, 10) + ".yaml" + fpath = filepath.Join(os.TempDir(), fname) + + // декодируем файл в промежуточное представление + ast, err := yamlparser.ParseBytes(goldenYAML, 0) + if err != nil { + return "", nil, fmt.Errorf("cannot build YAML AST: %w", err) + } + + // модифицируем YAML дерево + modifications, err = applyYAMLModifications(rnd, ast) + if err != nil { + return "", nil, fmt.Errorf("cannot perform YAML tree modifications: %w", err) + } + // обогощаем информацию о модификациях + for i, m := range modifications { + m.message = fmt.Sprintf("%s:%d %s", fname, m.lineno, m.message) + modifications[i] = m + } + + // запишем модифицированные данные в файл + if err := os.WriteFile(fpath, []byte(ast.String()), 0444); err != nil { + return "", nil, fmt.Errorf("cannot write modified YAML file: %w", err) + } + return fpath, modifications, nil +} + +type yamlModification struct { + lineno int + message string +} + +func applyYAMLModifications(rnd *rand.Rand, root *yamlast.File) ([]yamlModification, error) { + if root == nil { + return nil, errors.New("root YAML node expected") + } + + funcs := []yamlModifierFunc{ + modifyYAMLNop, // с определенной вероятностью файл не будет модифицирован вообще + modifyYAMLSpecOS, + modifyYAMLRemoveRequired, + modifyYAMLPortOutOfRange, + modifyYAMLInvalidType, + } + + rnd.Shuffle(len(funcs), func(i, j int) { + funcs[i], funcs[j] = funcs[j], funcs[i] + }) + + modificationsCount := intInRange(rnd, 1, len(funcs)) + var modifications []yamlModification + for _, fn := range funcs[:modificationsCount] { + mods, err := fn(rnd, root) + if err != nil { + return nil, fmt.Errorf("cannot apply modification: %w", err) + } + modifications = append(modifications, mods...) + } + + return modifications, nil +} + +// yamlModifierFunc функция, которая умеет модифицировать одну или более ноду YAML дерева +type yamlModifierFunc func(rnd *rand.Rand, root *yamlast.File) ([]yamlModification, error) + +// modifyYAMLNop не делает с YAML деревом ничего +func modifyYAMLNop(_ *rand.Rand, root *yamlast.File) ([]yamlModification, error) { + return nil, nil +} + +// modifyYAMLSpecOS заменяет значение `spec.os` на не валидное +func modifyYAMLSpecOS(_ *rand.Rand, root *yamlast.File) ([]yamlModification, error) { + badValue := random.ASCIIString(3, 10) + + path, err := yaml.PathString("$.spec.os") + if err != nil { + return nil, fmt.Errorf("bad field path given: %w", err) + } + + node, err := path.FilterFile(root) + if err != nil { + return nil, fmt.Errorf("cannot filter 'spec.os' node: %w", err) + } + + lineno := node.GetToken().Position.Line + path.ReplaceWithReader(root, strings.NewReader(badValue)) + return []yamlModification{ + { + lineno: lineno, + message: fmt.Sprintf("%s has unsupported value '%s'", basename(node.GetPath()), badValue), + }, + }, nil +} + +// modifyYAMLRemoveRequired удаляет случайную обязательную ноду +func modifyYAMLRemoveRequired(rnd *rand.Rand, root *yamlast.File) ([]yamlModification, error) { + paths := []string{ + "$.spec.containers[0].name", + "$.metadata.name", + } + + path, err := yaml.PathString(paths[rnd.Intn(len(paths))]) + if err != nil { + return nil, fmt.Errorf("bad field path given: %w", err) + } + + node, err := path.FilterFile(root) + if err != nil { + return nil, fmt.Errorf("cannot filter node by path '%s': %w", path, err) + } + + lineno := node.GetToken().Position.Line + path.ReplaceWithReader(root, strings.NewReader(`""`)) + return []yamlModification{ + { + lineno: lineno, + message: fmt.Sprintf("%s is required", basename(node.GetPath())), + }, + }, nil +} + +// modifyYAMLPortOutOfRange устанавливает значение порта за пределами границ +func modifyYAMLPortOutOfRange(rnd *rand.Rand, root *yamlast.File) ([]yamlModification, error) { + paths := []string{ + "$.spec.containers[0].ports[0].containerPort", + "$.spec.containers[0].readinessProbe.httpGet.port", + "$.spec.containers[0].livenessProbe.httpGet.port", + } + + port := rnd.Intn(100000) + if port < 65536 { + port *= -1 + } + + path, err := yaml.PathString(paths[rnd.Intn(len(paths))]) + if err != nil { + return nil, fmt.Errorf("bad field path given: %w", err) + } + + node, err := path.FilterFile(root) + if err != nil { + return nil, fmt.Errorf("cannot filter node by path '%s': %w", path, err) + } + + lineno := node.GetToken().Position.Line + path.ReplaceWithReader(root, strings.NewReader(fmt.Sprint(port))) + return []yamlModification{ + { + lineno: lineno, + message: fmt.Sprintf("%s value out of range", basename(node.GetPath())), + }, + }, nil +} + +// modifyYAMLInvalidType меняет тип на недопустимый +func modifyYAMLInvalidType(rnd *rand.Rand, root *yamlast.File) ([]yamlModification, error) { + paths := []string{ + "$.spec.containers[0].resources.limits.cpu", + "$.spec.containers[0].resources.requests.cpu", + } + + path, err := yaml.PathString(paths[rnd.Intn(len(paths))]) + if err != nil { + return nil, fmt.Errorf("bad field path given: %w", err) + } + + node, err := path.FilterFile(root) + if err != nil { + return nil, fmt.Errorf("cannot filter node by path '%s': %w", path, err) + } + + lineno := node.GetToken().Position.Line + path.ReplaceWithReader(root, strings.NewReader(`"`+node.String()+`"`)) + return []yamlModification{ + { + lineno: lineno, + message: fmt.Sprintf("%s must be int", basename(node.GetPath())), + }, + }, nil +} + +func basename(path string) string { + idx := strings.LastIndex(path, ".") + if idx == -1 { + return path + } + return path[idx+1:] +} diff --git a/cmd/devopsmastertest/main_test.go b/cmd/devopsmastertest/main_test.go index 3a69600..bd1ed1a 100644 --- a/cmd/devopsmastertest/main_test.go +++ b/cmd/devopsmastertest/main_test.go @@ -1,6 +1,6 @@ package main -//go:generate go test -c -o=../../bin/devopsreskill +//go:generate go test -c -o=../../bin/devopsmastertest import ( "os" @@ -16,3 +16,7 @@ func TestMain(m *testing.M) { func TestLesson01(t *testing.T) { suite.Run(t, new(Lesson01Suite)) } + +func TestLesson02(t *testing.T) { + suite.Run(t, new(Lesson02Suite)) +} diff --git a/go.mod b/go.mod index ebf06f3..3b6e388 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.22.5 require ( github.com/go-resty/resty/v2 v2.7.0 + github.com/goccy/go-yaml v1.12.0 github.com/gofrs/uuid v4.3.0+incompatible github.com/google/pprof v0.0.0-20220829040838-70bd9ae97f40 github.com/jackc/pgx v3.6.2+incompatible @@ -21,10 +22,13 @@ require ( github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/cockroachdb/apd v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.10.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.4.2 // indirect github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect github.com/lib/pq v1.10.7 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect @@ -32,6 +36,8 @@ require ( golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0104864..25fdb7d 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,18 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= +github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -29,8 +39,14 @@ github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yI github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= @@ -90,6 +106,8 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -100,6 +118,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -123,6 +143,7 @@ golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=