本文档采用自动化机器翻译技术翻译。 尽管我们力求提供准确的译文,但不对翻译内容的完整性、准确性或可靠性作出任何保证。 若出现任何内容不一致情况,请以原始 英文 版本为准,且原始英文版本为权威文本。

这是尚未发布的文档。 Admission Controller 1.34-dev.

定义策略设置

首先,您需要定义一个保存策略设置的结构。

您可以通过修改`settings.go`文件中的代码来实现(来自您本地的Go策略模板)。 您需要在`import`部分添加两行额外的代码,修改`Settings`结构,并添加`RegularExpression`结构。

它应与以下代码匹配:

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
}

在`settings.go`中定义的`Settings`结构中不再需要`DeniedNames`。

由于`DeniedNames`不再被定义,您还应该在`settings.go`中删除函数`IsNameDefined`。 您还应该在`settings_test.go`、`TestIsNameDenied`中去除引用它的函数。

您正在使用`regexp`软件包来处理正则表达式对象,并使用https://github.com/deckarep/golang-set[mapset.Set]类型来存储被拒绝标签的列表。

由于`regexp.Regexp`不处理解组,您需要定义自定义函数来处理正则表达式的编组和解组:

// 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
}

此外,您需要`UnmarshalJSON`方法来处理`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
}

构建`Settings`实例

一个Admission Controller策略暴露两个不同的函数,这些函数接收策略设置作为输入:

  • validate:当Kubernetes对象需要通过策略进行验证时,请使用此函数。 这些设置是https://pkg.go.dev/github.com/kubewarden/policy-sdk-go@v0.2.1/protocol#ValidationRequest[ValidationRequest]对象的一部分。

  • validate_settings:当策略首次被Admission Controller加载时,请调用此函数。 该函数接收策略设置作为输入并检查有效性。

您需要创建一个辅助函数,从JSON有效负载开始创建`Settings`对象:

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
}

所有的`mapset.Set`对象都是使用https://pkg.go.dev/github.com/deckarep/golang-set?utm_source=godoc#NewThreadUnsafeSet[线程不安全变体]创建的。 WebAssembly代码在单个线程中执行,因此没有并发问题。

WebAssembly标准尚未涵盖线程。 有关更多详细信息,请参见https://github.com/WebAssembly/threads[官方提案]。

实现`Settings`验证

所有Admission Controller策略必须实现设置验证。

您可以通过向`Settings`实例添加`Valid`方法来实现这一点:

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
}

`Valid`方法确保没有“被拒绝”标签也属于“受限”映射。 使用由`mapset.Set`提供的`Intersect`方法简化了检查。

`Valid`方法调用是在已经实例化的`Setting`对象上进行的。 这意味着用户提供的正则表达式的验证已经在`Settings`解组器中进行。

最后,您需要将脚手架提供的`validateSettings`函数更改为如下所示:

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)))
}

您可以看到,该函数利用了由https://github.com/kubewarden/policy-sdk-go[Admission Controller的SDK提供的辅助函数。]

测试设置代码

确保您编写的代码有良好的测试覆盖率是很重要的。 您使用的来自脚手架的代码附带了一系列在`settings_test.go`文件中定义的单元测试。

您必须更改此文件的内容以反映`Settings`类的新行为。

包含您正在使用的Go软件包:

import (
    "testing"

    "encoding/json"

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

您可以通过编写一个单元测试开始,确保您可以从`ValidationRequest`对象分配一个`Settings`实例:

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())
    }
}

接下来,您需要一个测试,检查当用户提供一个损坏的正则表达式时不会生成`Settings`实例:

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")
    }
}

现在,您可以定义一个测试来检查`validate_settings`入口点的行为。

您查看由您的`validateSettings`函数返回的`SettingsValidationResponse`对象:

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)
    }
}

最后,您编写两个测试来检查`validateSettings`函数是否以正确的消息拒绝无效设置:

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)
    }
}

现在,您可以使用以下命令运行到目前为止定义的测试:

go test -v settings.go settings_test.go

所有测试将通过,输出如下:

=== 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

您现在可以在下一部分实现实际的验证代码。