Skip to content

Commit

Permalink
devopsmastertest lesson 2 (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
bbrodriges authored Oct 2, 2024
1 parent 4bcd447 commit 3d1ff3f
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 1 deletion.
31 changes: 31 additions & 0 deletions cmd/devopsmastertest/kuber_golden.yaml
Original file line number Diff line number Diff line change
@@ -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"
270 changes: 270 additions & 0 deletions cmd/devopsmastertest/lesson02_test.go
Original file line number Diff line number Diff line change
@@ -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:]
}
6 changes: 5 additions & 1 deletion cmd/devopsmastertest/main_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package main

//go:generate go test -c -o=../../bin/devopsreskill
//go:generate go test -c -o=../../bin/devopsmastertest

import (
"os"
Expand All @@ -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))
}
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,17 +22,22 @@ 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
golang.org/x/crypto v0.26.0 // indirect
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
)
Loading

0 comments on commit 3d1ff3f

Please sign in to comment.