Este documento foi traduzido usando tecnologia de tradução automática de máquina. Sempre trabalhamos para apresentar traduções precisas, mas não oferecemos nenhuma garantia em relação à integridade, precisão ou confiabilidade do conteúdo traduzido. Em caso de qualquer discrepância, a versão original em inglês prevalecerá e constituirá o texto official.

Esta é uma documentação não divulgada para Admission Controller 1.34-dev.

Definindo configurações de política

Primeiramente, você precisa definir a estrutura que contém as configurações de política.

Você faz isso modificando o código no arquivo settings.go (da sua versão local do modelo de política Go). Você precisa adicionar duas linhas extras à seção import, alterar a estrutura Settings e adicionar a estrutura RegularExpression.

Deve corresponder ao seguinte código:

import (
    "encoding/json"
    "fmt"
    "regexp"

    mapset "github.com/deckarep/golang-set/v2"
    kubewarden "github.com/kubewarden/policy-sdk-go"
    kubewarden_protocol "github.com/kubewarden/policy-sdk-go/protocol"
)

type Settings struct {
    DeniedLabels      mapset.Set[string]            `+json:"denied_labels"+`
    ConstrainedLabels map[string]*RegularExpression `+json:"constrained_labels"+`
}

type RegularExpression struct {
    *regexp.Regexp
}

DeniedNames não é mais necessário na estrutura Settings definida em settings.go.

Como DeniedNames não está mais definido, você também deve excluir a função IsNameDefined em settings.go. Você também deve remover a função que a referencia em settings_test.go, TestIsNameDenied.

Você está usando o pacote regexp para manipular objetos de expressões regulares e o tipo mapset.Set para armazenar a lista de rótulos negados.

Como regexp.Regexp não manipula desserialização, você precisa definir funções personalizadas para manipular a serialização e desserialização de expressões regulares:

// UnmarshalText satisfies the encoding.TextMarshaler interface,
// also used by json.Unmarshal.
func (r *RegularExpression) UnmarshalText(text []byte) error {
    nativeRegExp, err := regexp.Compile(string(text))
    if err != nil {
        return err
    }
    r.Regexp = nativeRegExp
    return nil
}

// MarshalText satisfies the encoding.TextMarshaler interface,
// also used by json.Marshal.
func (r *RegularExpression) MarshalText() ([]byte, error) {
    if r.Regexp != nil {
        return []byte(r.Regexp.String()), nil
    }

    return nil, nil
}

Além disso, você precisa do método UnmarshalJSON para lidar com a desserialização da struct Settings:

func (s *Settings) UnmarshalJSON(data []byte) error {
    rawSettings := struct {
        DeniedLabels      []string          `+json:"denied_labels"+`
        ConstrainedLabels map[string]*RegularExpression `+json:"constrained_labels"+`
    }{}

    err := json.Unmarshal(data, &rawSettings)
    if err != nil {
        return err
    }

    s.DeniedLabels = mapset.NewThreadUnsafeSet[string](rawSettings.DeniedLabels...)
    s.ConstrainedLabels = rawSettings.ConstrainedLabels

    return nil
}

Construindo instâncias de Settings

Uma política Admission Controller expõe duas funções diferentes que recebem as configurações de política como entrada:

  • validate: Use esta função quando o objeto Kubernetes requer validação pela política. As configurações são parte de um objeto ValidationRequest.

  • validate_settings: Chame esta função quando a política for carregada pela primeira vez por Admission Controller. A função recebe as configurações de política como entrada e verifica a validade.

Você precisa criar uma função auxiliar que cria um objeto Settings a partir do payload JSON :

func NewSettingsFromValidationReq(validationReq *kubewarden_protocol.ValidationRequest) (Settings, error) {
    settings := Settings{}
    err := json.Unmarshal(validationReq.Settings, &settings)
    if err != nil {
        return Settings{}, err
    }

    return settings, nil
}

Todos os mapset.Set objetos são criados usando o variante não segura para threads. O código WebAssembly é executado em uma única thread, portanto, não há problemas de concorrência.

O padrão WebAssembly ainda não cobre threads. Veja proposta oficial para mais detalhes.

Implementando a validação de Settings

Todas as políticas de Admission Controller devem implementar a validação de configurações.

Você faz isso adicionando um método Valid às instâncias de Settings:

func (s *Settings) Valid() (bool, error) {
    constrainedLabels := mapset.NewThreadUnsafeSet[string]()

    for label := range s.ConstrainedLabels {
        constrainedLabels.Add(label)
    }

    constrainedAndDenied := constrainedLabels.Intersect(s.DeniedLabels)
    if constrainedAndDenied.Cardinality() != 0 {
        return false,
            fmt.Errorf("These labels cannot be constrained and denied at the same time: %v", constrainedAndDenied)
    }

    return true, nil
}

O método Valid garante que nenhum rótulo "negado" também faça parte do mapa "restrito". O uso do método Intersect fornecido por mapset.Set simplifica a verificação.

A invocação do método Valid é em um objeto Setting já instanciado. Isso significa que a validação da expressão regular fornecida pelo usuário já ocorreu no unmarshaler de Settings.

Finalmente, você precisa da função validateSettings, fornecida pela estrutura, para mudar para parecer assim:

func validateSettings(payload []byte) ([]byte, error) {
    settings := Settings{}
    err := json.Unmarshal(payload, &settings)
    if err != nil {
        return kubewarden.RejectSettings(
            kubewarden.Message(fmt.Sprintf("Provided settings are not valid: %v", err)))
    }

    valid, err := settings.Valid()
    if valid {
        return kubewarden.AcceptSettings()
    }

    return kubewarden.RejectSettings(
        kubewarden.Message(fmt.Sprintf("Provided settings are not valid: %v", err)))
}

Você pode ver que a função aproveita as funções auxiliares fornecidas pelo SDK de Admission Controller.

Testando o código de configurações

É importante ter uma boa cobertura de testes do código que você escreve. O código que você está usando, da estrutura, vem com uma série de testes de unidade definidos no arquivo settings_test.go.

Você precisa alterar o conteúdo deste arquivo para refletir o novo comportamento da classe Settings.

Inclua os pacotes Go que você está usando:

import (
    "testing"

    "encoding/json"

    kubewarden_protocol "github.com/kubewarden/policy-sdk-go/protocol"
)

Você pode começar escrevendo um teste unitário que garante que você pode atribuir uma instância de Settings de um objeto ValidationRequest:

func TestParseValidSettings(t *testing.T) {
    settingsJSON := []byte(`
        {
            "denied_labels": [ "foo", "bar" ],
            "constrained_labels": {
                    "cost-center": "cc-\\d+"
            }
        }`)

    settings := Settings{}
    err := json.Unmarshal(settingsJSON, &settings)
    if err != nil {
        t.Errorf("Unexpected error %+v", err)
    }

    expected_denied_labels := []string{"foo", "bar"}
    for _, exp := range expected_denied_labels {
        if !settings.DeniedLabels.Contains(exp) {
            t.Errorf("Missing value %s", exp)
        }
    }

    re, found := settings.ConstrainedLabels["cost-center"]
    if !found {
        t.Error("Didn't find the expected constrained label")
    }

    expected_regexp := `+cc-\d++`
    if re.String() != expected_regexp {
        t.Errorf("Expected regexp to be %v - got %v instead",
            expected_regexp, re.String())
    }
}

Em seguida, você precisa de um teste que verifica se uma instância de Settings não é gerada quando o usuário fornece uma expressão regular quebrada:

func TestParseSettingsWithInvalidRegexp(t *testing.T) {
    settingsJSON := []byte(`
        {
            "denied_labels": [ "foo", "bar" ],
            "constrained_labels": {
                    "cost-center": "cc-[a+"
            }
        }`)

    err := json.Unmarshal(settingsJSON, &Settings{})
    if err == nil {
        t.Errorf("Didn't get expected error")
    }
}

Agora, você pode definir um teste que verifica o comportamento do ponto de entrada validate_settings.

Você observa o objeto SettingsValidationResponse retornado pela sua função validateSettings:

func TestDetectValidSettings(t *testing.T) {
    settingsJSON := []byte(`
    {
        "denied_labels": [ "foo", "bar" ],
        "constrained_labels": {
            "cost-center": "cc-\\d+"
        }
    }`)

    responsePayload, err := validateSettings(settingsJSON)
    if err != nil {
        t.Errorf("Unexpected error %+v", err)
    }

    var response kubewarden_protocol.SettingsValidationResponse
    if err := json.Unmarshal(responsePayload, &response); err != nil {
        t.Errorf("Unexpected error: %+v", err)
    }

    if !response.Valid {
        t.Errorf("Expected settings to be valid: %s", *response.Message)
    }
}

Finalmente, você escreve mais dois testes para verificar se a função validateSettings rejeita configurações inválidas com as mensagens corretas:

func TestDetectNotValidSettingsDueToBrokenRegexp(t *testing.T) {
    settingsJSON := []byte(`
    {
        "denied_labels": [ "foo", "bar" ],
        "constrained_labels": {
            "cost-center": "cc-[a+"
        }
    }
    `)

    responsePayload, err := validateSettings(settingsJSON)
    if err != nil {
        t.Errorf("Unexpected error %+v", err)
    }

    var response kubewarden_protocol.SettingsValidationResponse
    if err := json.Unmarshal(responsePayload, &response); err != nil {
        t.Errorf("Unexpected error: %+v", err)
    }

    if response.Valid {
        t.Error("Expected settings to not be valid")
    }

    if *response.Message != "Provided settings are not valid: error parsing regexp: missing closing ]: `+[a++`" {
        t.Errorf("Unexpected validation error message: %s", *response.Message)
    }
}

func TestDetectNotValidSettingsDueToConflictingLabels(t *testing.T) {
    settingsJSON := []byte(`
    {
        "denied_labels": [ "foo", "bar", "cost-center" ],
        "constrained_labels": {
            "cost-center": ".*"
        }
    }`)
    responsePayload, err := validateSettings(settingsJSON)
    if err != nil {
        t.Errorf("Unexpected error %+v", err)
    }

    var response kubewarden_protocol.SettingsValidationResponse
    if err := json.Unmarshal(responsePayload, &response); err != nil {
        t.Errorf("Unexpected error: %+v", err)
    }

    if response.Valid {
        t.Error("Expected settings to not be valid")
    }

    if *response.Message != "Provided settings are not valid: These labels cannot be constrained and denied at the same time: Set{cost-center}" {
        t.Errorf("Unexpected validation error message: %s", *response.Message)
    }
}

Agora você pode executar os testes que definiu até agora usando o seguinte comando:

go test -v settings.go settings_test.go

Todos os testes passarão com a seguinte saída:

=== RUN   TestParseValidSettings
--- PASS: TestParseValidSettings (0.00s)
=== RUN   TestParseSettingsWithInvalidRegexp
--- PASS: TestParseSettingsWithInvalidRegexp (0.00s)
=== RUN   TestDetectValidSettings
--- PASS: TestDetectValidSettings (0.00s)
=== RUN   TestDetectNotValidSettingsDueToBrokenRegexp
--- PASS: TestDetectNotValidSettingsDueToBrokenRegexp (0.00s)
=== RUN   TestDetectNotValidSettingsDueToConflictingLabels
--- PASS: TestDetectNotValidSettingsDueToConflictingLabels (0.00s)
PASS
ok      command-line-arguments    0.002s

Agora você pode implementar o código de validação real na próxima seção.