├── .gitignore
├── .license-header.go.txt
├── .markdownlint.json
├── .dockerignore
├── pkg
├── state
│ ├── registry
│ │ ├── registry.go
│ │ ├── resource_test.go
│ │ ├── namespace_test.go
│ │ ├── namespace.go
│ │ └── resource.go
│ ├── protobuf
│ │ ├── protobuf.go
│ │ ├── client
│ │ │ ├── id_query.go
│ │ │ ├── errors.go
│ │ │ └── label_query.go
│ │ ├── server
│ │ │ └── helpers.go
│ │ └── runtime_test.go
│ ├── conformance
│ │ ├── conformance.go
│ │ ├── watch.go
│ │ └── resources.go
│ ├── impl
│ │ ├── inmem
│ │ │ ├── build.go
│ │ │ ├── backing_store.go
│ │ │ ├── errors_test.go
│ │ │ ├── options.go
│ │ │ └── errors.go
│ │ ├── store
│ │ │ ├── store.go
│ │ │ ├── compression
│ │ │ │ ├── zstd.go
│ │ │ │ └── compression.go
│ │ │ ├── protobuf.go
│ │ │ ├── bolt
│ │ │ │ ├── bbolt.go
│ │ │ │ ├── conformance_test.go
│ │ │ │ ├── namespaced.go
│ │ │ │ └── bbolt_test.go
│ │ │ └── protobuf_test.go
│ │ └── namespaced
│ │ │ └── namespaced_test.go
│ ├── wrap_test.go
│ ├── filter_test.go
│ └── condition.go
├── resource
│ ├── list.go
│ ├── owner.go
│ ├── kind.go
│ ├── pointer.go
│ ├── reference.go
│ ├── meta
│ │ ├── sensitivity.go
│ │ ├── core.go
│ │ ├── spec
│ │ │ └── sensitivity.go
│ │ ├── namespace_test.go
│ │ ├── namespace.go
│ │ └── resource_definition.go
│ ├── kvutils
│ │ └── kvutils.go
│ ├── rtestutils
│ │ ├── rtestutils.go
│ │ ├── ids.go
│ │ └── errors.go
│ ├── resource_test.go
│ ├── internal
│ │ ├── kv
│ │ │ └── kv_test.go
│ │ └── compare
│ │ │ ├── compare.go
│ │ │ └── compare_test.go
│ ├── annotations.go
│ ├── id_query.go
│ ├── phase.go
│ ├── any_test.go
│ ├── finalizer_test.go
│ ├── finalizer.go
│ ├── annotations_test.go
│ ├── id_query_test.go
│ ├── tombstone.go
│ ├── protobuf
│ │ ├── protobuf.go
│ │ ├── registry_test.go
│ │ ├── spec_unmarshal_test.go
│ │ ├── yaml.go
│ │ ├── resource_test.go
│ │ ├── yaml_test.go
│ │ └── spec.go
│ ├── version.go
│ ├── resource.go
│ ├── any.go
│ ├── labels.go
│ ├── handle
│ │ └── handle.go
│ └── labels_test.go
├── controller
│ ├── runtime
│ │ ├── internal
│ │ │ ├── qruntime
│ │ │ │ ├── internal
│ │ │ │ │ ├── containers
│ │ │ │ │ │ ├── containers.go
│ │ │ │ │ │ ├── slice_set.go
│ │ │ │ │ │ └── slice_set_test.go
│ │ │ │ │ ├── timer
│ │ │ │ │ │ ├── resettable_test.go
│ │ │ │ │ │ └── resettable.go
│ │ │ │ │ └── queue
│ │ │ │ │ │ └── queue_test.go
│ │ │ │ ├── backoff.go
│ │ │ │ ├── watch.go
│ │ │ │ └── qitem.go
│ │ │ ├── reduced
│ │ │ │ ├── filter.go
│ │ │ │ └── reduced.go
│ │ │ ├── cache
│ │ │ │ └── errors.go
│ │ │ ├── rruntime
│ │ │ │ ├── tracking_pool.go
│ │ │ │ ├── run.go
│ │ │ │ ├── output_tracker.go
│ │ │ │ ├── watch.go
│ │ │ │ └── state.go
│ │ │ ├── adapter
│ │ │ │ └── adapter.go
│ │ │ └── dependency
│ │ │ │ └── bench_test.go
│ │ ├── metrics
│ │ │ ├── metrics.go
│ │ │ └── state.go
│ │ └── options
│ │ │ └── options.go
│ ├── conformance
│ │ ├── conformance.go
│ │ ├── resource.go
│ │ └── resources.go
│ ├── generic
│ │ ├── transform
│ │ │ ├── transform.go
│ │ │ ├── metrics.go
│ │ │ └── resource_test.go
│ │ ├── generic.go
│ │ └── destroy
│ │ │ └── destroy.go
│ ├── controller.go
│ └── dependency.go
├── safe
│ ├── safe.go
│ ├── writer.go
│ ├── util.go
│ └── reader.go
├── logging
│ └── logging.go
├── future
│ ├── future_test.go
│ └── future.go
├── task
│ ├── runner_test.go
│ ├── task_test.go
│ ├── task.go
│ └── runner.go
└── keystorage
│ └── testdata
│ └── public.key
├── .codecov.yml
├── hack
├── release.toml
├── git-chglog
│ ├── CHANGELOG.tpl.md
│ └── config.yaml
└── govulncheck.sh
├── .github
├── workflows
│ ├── lock.yml
│ └── stale.yml
└── renovate.json
├── api
└── key_storage
│ └── key_storage.proto
├── .kres.yaml
├── .conform.yaml
├── go.mod
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2020-08-28T20:43:11Z by kres 292ed36-dirty.
4 |
5 | _out
6 |
--------------------------------------------------------------------------------
/.license-header.go.txt:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2020-08-28T20:43:11Z by kres 292ed36-dirty.
4 |
5 | {
6 | "MD013": false,
7 | "MD033": false,
8 | "default": true
9 | }
10 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2025-09-01T12:59:11Z by kres 784fa1f.
4 |
5 | *
6 | !api
7 | !cmd
8 | !pkg
9 | !go.mod
10 | !go.sum
11 | !.golangci.yml
12 | !README.md
13 | !.markdownlint.json
14 | !hack/govulncheck.sh
15 |
--------------------------------------------------------------------------------
/pkg/state/registry/registry.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package registry provides registries for namespaces and resource definitions.
6 | package registry
7 |
--------------------------------------------------------------------------------
/pkg/resource/list.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | // List is a list of resources.
8 | type List struct {
9 | Items []Resource
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/resource/owner.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | // Owner of the resource (controller which manages the resource).
8 | type Owner = string
9 |
--------------------------------------------------------------------------------
/pkg/state/protobuf/protobuf.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package protobuf provides wrappers/adapters between gRPC service and state.CoreState.
6 | package protobuf
7 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/internal/containers/containers.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package containers provides helper containers for qruntime.
6 | package containers
7 |
--------------------------------------------------------------------------------
/pkg/state/conformance/conformance.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package conformance implements tests which verify conformance of the implementation with the spec.
6 | package conformance
7 |
--------------------------------------------------------------------------------
/pkg/controller/conformance/conformance.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package conformance implements tests which verify conformance of the implementation with the spec.
6 | package conformance
7 |
--------------------------------------------------------------------------------
/pkg/resource/kind.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | // Kind is a Pointer minus resource ID.
8 | type Kind interface {
9 | Namespace() Namespace
10 | Type() Type
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/resource/pointer.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | // Pointer is a Reference minus resource version.
8 | type Pointer interface {
9 | Kind
10 |
11 | ID() ID
12 | }
13 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2024-01-30T09:06:08Z by kres latest.
4 |
5 | codecov:
6 | require_ci_to_pass: false
7 |
8 | coverage:
9 | status:
10 | project:
11 | default:
12 | target: 45%
13 | threshold: 0.5%
14 | base: auto
15 | if_ci_failed: success
16 | patch: off
17 |
18 | comment: false
19 |
--------------------------------------------------------------------------------
/pkg/resource/reference.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import "fmt"
8 |
9 | // Reference to a resource.
10 | type Reference interface {
11 | fmt.Stringer
12 |
13 | Pointer
14 | Version() Version
15 | }
16 |
--------------------------------------------------------------------------------
/hack/release.toml:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2021-06-21T09:20:43Z by kres latest.
4 |
5 |
6 | # commit to be tagged for the new release
7 | commit = "HEAD"
8 |
9 | project_name = "runtime"
10 | github_repo = "cosi-project/runtime"
11 | match_deps = "^github.com/(cosi-project/[a-zA-Z0-9-]+)$"
12 |
13 | # previous = -
14 | # pre_release = true
15 |
16 | # [notes]
17 |
--------------------------------------------------------------------------------
/pkg/resource/meta/sensitivity.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package meta
6 |
7 | import "github.com/cosi-project/runtime/pkg/resource/meta/spec"
8 |
9 | // Sensitivity values.
10 | const (
11 | NonSensitive = spec.NonSensitive
12 | Sensitive = spec.Sensitive
13 | )
14 |
--------------------------------------------------------------------------------
/pkg/resource/kvutils/kvutils.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package kvutils provides utilities to internal/kv package.
6 | package kvutils
7 |
8 | // TempKV is a temporary key-value store.
9 | type TempKV interface {
10 | Delete(key string)
11 | Set(key, value string)
12 | Get(key string) (string, bool)
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/state/impl/inmem/build.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package inmem
6 |
7 | import (
8 | "github.com/cosi-project/runtime/pkg/resource"
9 | "github.com/cosi-project/runtime/pkg/state"
10 | )
11 |
12 | // Build a local state for namespace.
13 | func Build(ns resource.Namespace) state.CoreState { //nolint:ireturn
14 | return NewState(ns)
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/resource/rtestutils/rtestutils.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package rtestutils provides utilities for testing with resource API.
6 | package rtestutils
7 |
8 | import (
9 | "github.com/cosi-project/runtime/pkg/resource/meta"
10 | )
11 |
12 | // ResourceWithRD is an alias for meta.ResourceWithRD.
13 | type ResourceWithRD = meta.ResourceWithRD
14 |
--------------------------------------------------------------------------------
/.github/workflows/lock.yml:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2025-10-31T08:08:31Z by kres cd5a938.
4 |
5 | "on":
6 | schedule:
7 | - cron: 0 2 * * *
8 | name: Lock old issues
9 | permissions:
10 | issues: write
11 | jobs:
12 | action:
13 | runs-on:
14 | - ubuntu-latest
15 | steps:
16 | - name: Lock old issues
17 | uses: dessant/lock-threads@v5.0.1
18 | with:
19 | issue-inactive-days: "60"
20 | log-output: "true"
21 | process-only: issues
22 |
--------------------------------------------------------------------------------
/pkg/resource/meta/core.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package meta provides definition of core metadata resources.
6 | package meta
7 |
8 | import "github.com/cosi-project/runtime/pkg/resource"
9 |
10 | // NamespaceName is the name of 'meta' namespace.
11 | const NamespaceName resource.Namespace = "meta"
12 |
13 | // Owner is the owner for the 'meta' objects.
14 | const Owner resource.Owner = "meta"
15 |
--------------------------------------------------------------------------------
/pkg/state/protobuf/client/id_query.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package client
6 |
7 | import (
8 | "github.com/cosi-project/runtime/api/v1alpha1"
9 | "github.com/cosi-project/runtime/pkg/resource"
10 | )
11 |
12 | func transformIDQuery(input resource.IDQuery) *v1alpha1.IDQuery {
13 | if input.Regexp == nil {
14 | return nil
15 | }
16 |
17 | return &v1alpha1.IDQuery{
18 | Regexp: input.Regexp.String(),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/controller/generic/transform/transform.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package transform provides a generic implementation of controller which transforms resources A into resources B.
6 | package transform
7 |
8 | // SkipReconcileTag is used to tag errors when reconciliation should be skipped without an error.
9 | //
10 | // It's useful when next reconcile event should bring things into order.
11 | type SkipReconcileTag struct{}
12 |
--------------------------------------------------------------------------------
/pkg/resource/resource_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | )
14 |
15 | func TestIsTombstone(t *testing.T) {
16 | t.Parallel()
17 |
18 | assert.True(t, resource.IsTombstone(new(resource.Tombstone)))
19 | assert.False(t, resource.IsTombstone((resource.Resource)(nil)))
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/store.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package store provides support for in-memory backing store implementations.
6 | package store
7 |
8 | import "github.com/cosi-project/runtime/pkg/resource"
9 |
10 | // Marshaler provides marshal/unmarshal for resources and backing store.
11 | type Marshaler interface {
12 | MarshalResource(resource.Resource) ([]byte, error)
13 | UnmarshalResource([]byte) (resource.Resource, error)
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/reduced/filter.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package reduced
6 |
7 | import "github.com/cosi-project/runtime/pkg/resource"
8 |
9 | // WatchFilter filters watches on reduced Metadata.
10 | type WatchFilter func(*Metadata) bool
11 |
12 | // FilterDestroyReady returns true if the Metadata is ready to be destroyed.
13 | func FilterDestroyReady(md *Metadata) bool {
14 | return md.Phase == resource.PhaseTearingDown && md.FinalizersEmpty
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/resource/meta/spec/sensitivity.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package spec
6 |
7 | // Sensitivity indicates how secret resource is.
8 | // The empty value represents a non-sensitive resource.
9 | type Sensitivity string
10 |
11 | // Sensitivity values.
12 | const (
13 | NonSensitive Sensitivity = ""
14 | Sensitive Sensitivity = "sensitive"
15 | )
16 |
17 | var allSensitivities = map[Sensitivity]struct{}{
18 | NonSensitive: {},
19 | Sensitive: {},
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/resource/internal/kv/kv_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package kv_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource/internal/kv"
13 | )
14 |
15 | func TestEqualSameLenEmptyValue(t *testing.T) {
16 | var kv1, kv2 kv.KV
17 |
18 | kv1.Set("a", "")
19 | kv2.Set("b", "")
20 |
21 | equal := kv1.Equal(kv2)
22 |
23 | assert.False(t, equal, "Expected kv1 and kv2 to be not equal")
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/cache/errors.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package cache
6 |
7 | import (
8 | "fmt"
9 |
10 | "github.com/cosi-project/runtime/pkg/resource"
11 | )
12 |
13 | //nolint:errname
14 | type eNotFound struct {
15 | error
16 | }
17 |
18 | func (eNotFound) NotFoundError() {}
19 |
20 | // ErrNotFound generates error compatible with state.ErrNotFound.
21 | func ErrNotFound(r resource.Pointer) error {
22 | return eNotFound{
23 | fmt.Errorf("resource %s doesn't exist", r),
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/safe/safe.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package safe provides a safe wrappers around the cosi runtime.
6 | package safe
7 |
8 | import "github.com/cosi-project/runtime/pkg/resource"
9 |
10 | func typeAssertOrZero[T resource.Resource](got resource.Resource, err error) (T, error) { //nolint:ireturn
11 | if err != nil {
12 | var zero T
13 |
14 | return zero, err
15 | }
16 |
17 | result, ok := got.(T)
18 | if !ok {
19 | var zero T
20 |
21 | return zero, typeMismatchErr(result, got)
22 | }
23 |
24 | return result, nil
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/controller/generic/generic.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package generic provides implementations of generic controllers.
6 | package generic
7 |
8 | import (
9 | "github.com/cosi-project/runtime/pkg/resource/meta"
10 | )
11 |
12 | // ResourceWithRD is an alias for meta.ResourceWithRD.
13 | type ResourceWithRD = meta.ResourceWithRD
14 |
15 | // NamedController is provides Name() method.
16 | type NamedController struct {
17 | ControllerName string
18 | }
19 |
20 | // Name implements controller.Controller interface.
21 | func (c *NamedController) Name() string {
22 | return c.ControllerName
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/resource/annotations.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import (
8 | "github.com/cosi-project/runtime/pkg/resource/internal/kv"
9 | )
10 |
11 | // Annotations is a set free-form of key-value pairs.
12 | //
13 | // Order of keys is not guaranteed.
14 | //
15 | // Annotations support copy-on-write semantics, so metadata copies share common Annotations as long as possible.
16 | type Annotations struct {
17 | kv.KV
18 | }
19 |
20 | // Equal checks Annotations for equality.
21 | func (annotations Annotations) Equal(other Annotations) bool {
22 | return annotations.KV.Equal(other.KV)
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/rruntime/tracking_pool.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package rruntime
6 |
7 | import "sync"
8 |
9 | type trackingOutputPool struct {
10 | pool sync.Pool
11 | }
12 |
13 | var trackingPoolInstance trackingOutputPool
14 |
15 | func (mp *trackingOutputPool) Get() map[outputTrackingID]struct{} {
16 | val := mp.pool.Get()
17 | if val, ok := val.(map[outputTrackingID]struct{}); ok {
18 | clear(val)
19 |
20 | return val
21 | }
22 |
23 | return map[outputTrackingID]struct{}{}
24 | }
25 |
26 | func (mp *trackingOutputPool) Put(x map[outputTrackingID]struct{}) {
27 | mp.pool.Put(x)
28 | }
29 |
--------------------------------------------------------------------------------
/hack/git-chglog/CHANGELOG.tpl.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ range .Versions }}
6 |
7 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }})
8 |
9 | {{ range .CommitGroups -}}
10 | ### {{ .Title }}
11 |
12 | {{ range .Commits -}}
13 | * {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
14 | {{ end }}
15 | {{ end -}}
16 |
17 | {{- if .NoteGroups -}}
18 | {{ range .NoteGroups -}}
19 | ### {{ .Title }}
20 |
21 | {{ range .Notes }}
22 | {{ .Body }}
23 | {{ end }}
24 | {{ end -}}
25 | {{ end -}}
26 | {{ end -}}
27 |
--------------------------------------------------------------------------------
/pkg/state/registry/resource_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package registry_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 |
12 | "github.com/cosi-project/runtime/pkg/state"
13 | "github.com/cosi-project/runtime/pkg/state/impl/inmem"
14 | "github.com/cosi-project/runtime/pkg/state/impl/namespaced"
15 | "github.com/cosi-project/runtime/pkg/state/registry"
16 | )
17 |
18 | func TestResourceRegistry(t *testing.T) {
19 | t.Parallel()
20 |
21 | r := registry.NewResourceRegistry(state.WrapCore(namespaced.NewState(inmem.Build)))
22 |
23 | assert.NoError(t, r.RegisterDefault(t.Context()))
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/state/registry/namespace_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package registry_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 |
12 | "github.com/cosi-project/runtime/pkg/state"
13 | "github.com/cosi-project/runtime/pkg/state/impl/inmem"
14 | "github.com/cosi-project/runtime/pkg/state/impl/namespaced"
15 | "github.com/cosi-project/runtime/pkg/state/registry"
16 | )
17 |
18 | func TestNamespaceRegistry(t *testing.T) {
19 | t.Parallel()
20 |
21 | r := registry.NewNamespaceRegistry(state.WrapCore(namespaced.NewState(inmem.Build)))
22 |
23 | assert.NoError(t, r.RegisterDefault(t.Context()))
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/controller/generic/transform/metrics.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package transform
6 |
7 | import (
8 | "expvar"
9 | )
10 |
11 | // TransformController specific metrics.
12 | var (
13 | // MetricReconcileCycles counts the number of times reconcile loop ran.
14 | MetricReconcileCycles = expvar.NewMap("reconcile_cycles")
15 |
16 | // MetricReconcileInputItems counts the number of resources reconciled overall.
17 | MetricReconcileInputItems = expvar.NewMap("reconcile_input_items")
18 |
19 | // MetricCycleBusy counts the number of seconds the controller was busy in the reconcile loop.
20 | MetricCycleBusy = expvar.NewMap("reconcile_busy")
21 | )
22 |
--------------------------------------------------------------------------------
/api/key_storage/key_storage.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package cosi.internal.key_storage;
4 |
5 | option go_package = "github.com/cosi-project/runtime/api/key_storage";
6 |
7 | // Storage is a main storage for keys in memory and in db.
8 | message Storage {
9 | StorageVersion storage_version = 1;
10 | map key_slots = 2;
11 | bytes keys_hmac_hash = 3;
12 | }
13 |
14 | // KeySlot is a single key slot in KeyStorage.
15 | message KeySlot {
16 | Algorithm algorithm = 1;
17 | bytes encrypted_key = 2;
18 | }
19 |
20 | // StorageVersion is a version of KeyStorage.
21 | enum StorageVersion {
22 | STORAGE_VERSION_UNSPECIFIED = 0;
23 | STORAGE_VERSION_1 = 1;
24 | }
25 |
26 | // Algorithm is an algorithm used for encryption.
27 | enum Algorithm {
28 | UNKNOWN = 0;
29 | PGP_AES_GCM_256 = 1; // PGP encrypted AES-256-GCM key
30 | }
31 |
--------------------------------------------------------------------------------
/hack/git-chglog/config.yaml:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2021-04-08T12:27:41Z by kres 7917d0d-dirty.
4 |
5 | style: github
6 | template: CHANGELOG.tpl.md
7 | info:
8 | title: CHANGELOG
9 | repository_url: https://github.com/cosi-project/runtime
10 | options:
11 | commits:
12 | # filters:
13 | # Type:
14 | # - feat
15 | # - fix
16 | # - perf
17 | # - refactor
18 | commit_groups:
19 | # title_maps:
20 | # feat: Features
21 | # fix: Bug Fixes
22 | # perf: Performance Improvements
23 | # refactor: Code Refactoring
24 | header:
25 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
26 | pattern_maps:
27 | - Type
28 | - Scope
29 | - Subject
30 | notes:
31 | keywords:
32 | - BREAKING CHANGE
33 |
--------------------------------------------------------------------------------
/pkg/logging/logging.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package logging defines logging helpers.
6 | package logging
7 |
8 | import (
9 | "go.uber.org/zap"
10 | "go.uber.org/zap/zapcore"
11 | )
12 |
13 | // Controller creates controller zap field.
14 | func Controller(name string) zap.Field {
15 | return zap.String("controller", name)
16 | }
17 |
18 | // DefaultLogger creates default logger.
19 | func DefaultLogger() *zap.Logger {
20 | config := zap.NewDevelopmentConfig()
21 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
22 | logger, _ := config.Build() //nolint:errcheck
23 |
24 | return logger.With(zap.String("component", "controller-runtime"))
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/state/wrap_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package state_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/suite"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | "github.com/cosi-project/runtime/pkg/state"
14 | "github.com/cosi-project/runtime/pkg/state/conformance"
15 | "github.com/cosi-project/runtime/pkg/state/impl/inmem"
16 | "github.com/cosi-project/runtime/pkg/state/impl/namespaced"
17 | )
18 |
19 | func TestWrapConformance(t *testing.T) {
20 | t.Parallel()
21 |
22 | suite.Run(t, &conformance.StateSuite{
23 | State: state.WrapCore(namespaced.NewState(inmem.Build)),
24 | Namespaces: []resource.Namespace{"default", "controller", "system", "runtime"},
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/backoff.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package qruntime
6 |
7 | import (
8 | "time"
9 |
10 | "github.com/cenkalti/backoff/v4"
11 | )
12 |
13 | func (adapter *Adapter) getBackoffInterval(item QKey) time.Duration {
14 | adapter.backoffsMu.Lock()
15 | defer adapter.backoffsMu.Unlock()
16 |
17 | bckoff, ok := adapter.backoffs[item]
18 | if !ok {
19 | bckoff = backoff.NewExponentialBackOff()
20 | bckoff.MaxElapsedTime = 0
21 | adapter.backoffs[item] = bckoff
22 | }
23 |
24 | return bckoff.NextBackOff()
25 | }
26 |
27 | func (adapter *Adapter) clearBackoff(item QKey) {
28 | adapter.backoffsMu.Lock()
29 | defer adapter.backoffsMu.Unlock()
30 |
31 | delete(adapter.backoffs, item)
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/resource/id_query.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import (
8 | "regexp"
9 | )
10 |
11 | // IDQuery is the query on the resource ID.
12 | type IDQuery struct {
13 | Regexp *regexp.Regexp
14 | }
15 |
16 | // Matches if the resource ID matches the ID query.
17 | func (query IDQuery) Matches(md Metadata) bool {
18 | if query.Regexp == nil {
19 | return true
20 | }
21 |
22 | return query.Regexp.MatchString(md.ID())
23 | }
24 |
25 | // IDQueryOption allows to build an IDQuery with functional parameters.
26 | type IDQueryOption func(*IDQuery)
27 |
28 | // IDRegexpMatch checks that the ID matches the regexp.
29 | func IDRegexpMatch(re *regexp.Regexp) IDQueryOption {
30 | return func(q *IDQuery) {
31 | q.Regexp = re
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/state/impl/namespaced/namespaced_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package namespaced_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/suite"
11 | "go.uber.org/goleak"
12 |
13 | "github.com/cosi-project/runtime/pkg/resource"
14 | "github.com/cosi-project/runtime/pkg/state"
15 | "github.com/cosi-project/runtime/pkg/state/conformance"
16 | "github.com/cosi-project/runtime/pkg/state/impl/inmem"
17 | "github.com/cosi-project/runtime/pkg/state/impl/namespaced"
18 | )
19 |
20 | func TestNamespacedConformance(t *testing.T) {
21 | t.Parallel()
22 |
23 | defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
24 |
25 | suite.Run(t, &conformance.StateSuite{
26 | State: state.WrapCore(namespaced.NewState(inmem.Build)),
27 | Namespaces: []resource.Namespace{"default", "controller", "system", "runtime"},
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/controller/controller.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package controller defines common interfaces to be implemented by the controllers and controller runtime.
6 | package controller
7 |
8 | import (
9 | "context"
10 |
11 | "go.uber.org/zap"
12 | )
13 |
14 | // Controller interface should be implemented by Controllers.
15 | type Controller interface {
16 | Name() string
17 | Inputs() []Input
18 | Outputs() []Output
19 |
20 | Run(context.Context, Runtime, *zap.Logger) error
21 | }
22 |
23 | // Engine is the entrypoint into Controller Runtime.
24 | type Engine interface {
25 | // RegisterController registers new controller.
26 | RegisterController(ctrl Controller) error
27 | // RegisterQController registers new QController.
28 | RegisterQController(ctrl QController) error
29 | // Run the controllers.
30 | Run(ctx context.Context) error
31 | }
32 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "description": "THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.",
4 | "prHeader": "Update Request | Renovate Bot",
5 | "extends": [
6 | ":dependencyDashboard",
7 | ":gitSignOff",
8 | ":semanticCommitScopeDisabled",
9 | "schedule:earlyMondays"
10 | ],
11 | "packageRules": [
12 | {
13 | "groupName": "dependencies",
14 | "matchUpdateTypes": [
15 | "major",
16 | "minor",
17 | "patch",
18 | "pin",
19 | "digest"
20 | ]
21 | },
22 | {
23 | "enabled": false,
24 | "matchFileNames": [
25 | "Dockerfile"
26 | ]
27 | },
28 | {
29 | "enabled": false,
30 | "matchFileNames": [
31 | ".github/workflows/*.yaml"
32 | ]
33 | }
34 | ],
35 | "separateMajorMinor": false
36 | }
37 |
--------------------------------------------------------------------------------
/.kres.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | kind: golang.Generate
3 | spec:
4 | experimentalFlags:
5 | - --experimental_allow_proto3_optional
6 | vtProtobufEnabled: true
7 | specs:
8 | - source: https://raw.githubusercontent.com/cosi-project/specification/a25fac056c642b32468b030387ab94c17bc3ba1d/proto/v1alpha1/resource.proto
9 | subdirectory: v1alpha1/
10 | genGateway: true
11 | external: false
12 | - source: https://raw.githubusercontent.com/cosi-project/specification/a25fac056c642b32468b030387ab94c17bc3ba1d/proto/v1alpha1/state.proto
13 | subdirectory: v1alpha1/
14 | genGateway: true
15 | external: false
16 | - source: https://raw.githubusercontent.com/cosi-project/specification/a25fac056c642b32468b030387ab94c17bc3ba1d/proto/v1alpha1/meta.proto
17 | subdirectory: v1alpha1/
18 | genGateway: true
19 | external: false
20 | - source: api/key_storage/key_storage.proto
21 | subdirectory: key_storage/
22 | genGateway: false
23 | external: false
24 | ---
25 | kind: service.CodeCov
26 | spec:
27 | targetThreshold: 45
28 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2025-10-31T08:08:31Z by kres cd5a938.
4 |
5 | "on":
6 | schedule:
7 | - cron: 30 1 * * *
8 | name: Close stale issues and PRs
9 | permissions:
10 | issues: write
11 | pull-requests: write
12 | jobs:
13 | stale:
14 | runs-on:
15 | - ubuntu-latest
16 | steps:
17 | - name: Close stale issues and PRs
18 | uses: actions/stale@v10.1.0
19 | with:
20 | close-issue-message: This issue was closed because it has been stalled for 7 days with no activity.
21 | days-before-issue-close: "5"
22 | days-before-issue-stale: "180"
23 | days-before-pr-close: "-1"
24 | days-before-pr-stale: "45"
25 | operations-per-run: "2000"
26 | stale-issue-message: This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.
27 | stale-pr-message: This PR is stale because it has been open 45 days with no activity.
28 |
--------------------------------------------------------------------------------
/pkg/resource/phase.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import "fmt"
8 |
9 | // Phase represents state of the resource.
10 | //
11 | // Resource might be either Running or TearingDown (waiting for the finalizers to be removed).
12 | type Phase int
13 |
14 | // Phase constants.
15 | const (
16 | PhaseRunning Phase = iota
17 | PhaseTearingDown
18 | )
19 |
20 | const (
21 | strPhaseRunning = "running"
22 | strPhaseTearingDown = "tearingDown"
23 | )
24 |
25 | func (ph Phase) String() string {
26 | return [...]string{strPhaseRunning, strPhaseTearingDown}[ph]
27 | }
28 |
29 | // ParsePhase from string representation.
30 | func ParsePhase(ph string) (Phase, error) {
31 | switch ph {
32 | case strPhaseRunning:
33 | return PhaseRunning, nil
34 | case strPhaseTearingDown:
35 | return PhaseTearingDown, nil
36 | default:
37 | return 0, fmt.Errorf("unknown phase: %v", ph)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/future/future_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package future_test
6 |
7 | import (
8 | "context"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/assert"
12 |
13 | "github.com/cosi-project/runtime/pkg/future"
14 | )
15 |
16 | func TestGo(t *testing.T) {
17 | t.Parallel()
18 |
19 | ctx, res := future.GoContext(t.Context(), func(context.Context) int {
20 | return 42
21 | })
22 |
23 | <-ctx.Done()
24 | assert.Equal(t, 42, <-res)
25 |
26 | ctx, cancel := context.WithCancel(t.Context())
27 | defer cancel()
28 |
29 | type result struct {
30 | err error
31 | value int
32 | }
33 |
34 | _, newRes := future.GoContext(ctx, func(ctx context.Context) result {
35 | <-ctx.Done()
36 |
37 | if ctx.Err() != nil {
38 | return result{err: ctx.Err()}
39 | }
40 |
41 | return result{value: 42}
42 | })
43 |
44 | cancel()
45 | assert.Equal(t, result{err: context.Canceled}, <-newRes)
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/resource/meta/namespace_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package meta_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/cosi-project/runtime/pkg/resource"
14 | "github.com/cosi-project/runtime/pkg/resource/meta"
15 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
16 | )
17 |
18 | func TestProtobufNamespace(t *testing.T) {
19 | ns := meta.NewNamespace("test", meta.NamespaceSpec{
20 | Description: "Test namespace",
21 | })
22 |
23 | protoR, err := protobuf.FromResource(ns)
24 | require.NoError(t, err)
25 |
26 | marshaled, err := protoR.Marshal()
27 | require.NoError(t, err)
28 |
29 | r, err := protobuf.Unmarshal(marshaled)
30 | require.NoError(t, err)
31 |
32 | back, err := protobuf.UnmarshalResource(r)
33 | require.NoError(t, err)
34 |
35 | assert.True(t, resource.Equal(ns, back))
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/state/protobuf/client/errors.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package client
6 |
7 | import "github.com/cosi-project/runtime/pkg/resource"
8 |
9 | //nolint:errname
10 | type eNotFound struct {
11 | error
12 | }
13 |
14 | func (eNotFound) NotFoundError() {}
15 |
16 | //nolint:errname
17 | type eConflict struct {
18 | error
19 | resource resource.Pointer
20 | }
21 |
22 | func (eConflict) ConflictError() {}
23 |
24 | func (e eConflict) GetResource() resource.Pointer {
25 | return e.resource
26 | }
27 |
28 | //nolint:errname
29 | type eOwnerConflict struct {
30 | eConflict
31 | }
32 |
33 | func (eOwnerConflict) OwnerConflictError() {}
34 |
35 | //nolint:errname
36 | type ePhaseConflict struct {
37 | eConflict
38 | }
39 |
40 | func (ePhaseConflict) PhaseConflictError() {}
41 |
42 | //nolint:errname
43 | type eInvalidWatchBookmark struct {
44 | error
45 | }
46 |
47 | func (eInvalidWatchBookmark) InvalidWatchBookmarkError() {}
48 |
--------------------------------------------------------------------------------
/pkg/controller/dependency.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package controller
6 |
7 | import "github.com/cosi-project/runtime/pkg/resource"
8 |
9 | // DependencyGraph is the exported information about controller/resources dependencies.
10 | type DependencyGraph struct {
11 | Edges []DependencyEdge
12 | }
13 |
14 | // DependencyEdgeType is edge type in controller graph.
15 | type DependencyEdgeType int
16 |
17 | // Controller graph edge types.
18 | const (
19 | EdgeOutputExclusive DependencyEdgeType = iota
20 | EdgeOutputShared
21 | EdgeInputStrong
22 | EdgeInputWeak
23 | EdgeInputDestroyReady
24 | EdgeInputQPrimary
25 | EdgeInputQMapped
26 | EdgeInputQMappedDestroyReady
27 | )
28 |
29 | // DependencyEdge represents relationship between controller and resource(s).
30 | type DependencyEdge struct {
31 | ControllerName string
32 |
33 | ResourceNamespace resource.Namespace
34 | ResourceType resource.Type
35 | ResourceID resource.ID
36 |
37 | EdgeType DependencyEdgeType
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/future/future.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package future provides a set of functions for observing the state of a running program.
6 | package future
7 |
8 | import "context"
9 |
10 | // GoContext runs a function in a goroutine and returns a channel that will receive the result. It will close the
11 | // channel and cancel the context when the function returns.
12 | func GoContext[T any](ctx context.Context, fn func(context.Context) T) (context.Context, <-chan T) {
13 | ctx, cancel := context.WithCancel(ctx)
14 | ch := make(chan T, 1)
15 |
16 | go func() {
17 | defer cancel()
18 | defer close(ch)
19 |
20 | ch <- fn(ctx)
21 | }()
22 |
23 | return ctx, ch
24 | }
25 |
26 | // Go runs a function in a goroutine and returns a channel that will receive the result.
27 | // It will close the channel when the function returns.
28 | func Go[T any](fn func() T) <-chan T {
29 | ch := make(chan T, 1)
30 |
31 | go func() {
32 | defer close(ch)
33 |
34 | ch <- fn()
35 | }()
36 |
37 | return ch
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/state/conformance/watch.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package conformance
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/siderolabs/gen/channel"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | "github.com/cosi-project/runtime/pkg/state"
14 | )
15 |
16 | func watchAggregateAdapter(ctx context.Context, useAggregated bool, st state.State, md resource.Kind, ch chan<- state.Event, options ...state.WatchKindOption) error {
17 | if useAggregated {
18 | aggCh := make(chan []state.Event)
19 |
20 | err := st.WatchKindAggregated(ctx, md, aggCh, options...)
21 | if err != nil {
22 | return err
23 | }
24 |
25 | go func() {
26 | for {
27 | select {
28 | case events := <-aggCh:
29 | for _, event := range events {
30 | if !channel.SendWithContext(ctx, ch, event) {
31 | return
32 | }
33 | }
34 | case <-ctx.Done():
35 | return
36 | }
37 | }
38 | }()
39 |
40 | return nil
41 | }
42 |
43 | return st.WatchKind(ctx, md, ch, options...)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/compression/zstd.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package compression
6 |
7 | import (
8 | "github.com/klauspost/compress/zstd"
9 | "github.com/siderolabs/gen/ensure"
10 | )
11 |
12 | // ZStd returns zstd compressor.
13 | func ZStd() Compressor {
14 | return &zstdCompressor{
15 | encoder: ensure.Value(zstd.NewWriter(
16 | nil,
17 | zstd.WithEncoderConcurrency(2),
18 | zstd.WithWindowSize(zstd.MinWindowSize),
19 | )),
20 | decoder: ensure.Value(zstd.NewReader(nil, zstd.WithDecoderConcurrency(0))),
21 | }
22 | }
23 |
24 | var _ Compressor = (*zstdCompressor)(nil)
25 |
26 | type zstdCompressor struct {
27 | encoder *zstd.Encoder
28 | decoder *zstd.Decoder
29 | }
30 |
31 | func (z *zstdCompressor) Compress(prefix, data []byte) ([]byte, error) {
32 | return z.encoder.EncodeAll(data, prefix), nil
33 | }
34 |
35 | func (z *zstdCompressor) Decompress(data []byte) ([]byte, error) {
36 | return z.decoder.DecodeAll(data, nil)
37 | }
38 |
39 | func (z *zstdCompressor) ID() byte {
40 | return 'z'
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/internal/timer/resettable_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package timer_test
6 |
7 | import (
8 | "testing"
9 | "time"
10 |
11 | "github.com/stretchr/testify/assert"
12 |
13 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/qruntime/internal/timer"
14 | )
15 |
16 | func TestResettableTimer(t *testing.T) {
17 | var tmr timer.ResettableTimer
18 |
19 | assert.Nil(t, tmr.C())
20 |
21 | tmr.Reset(0)
22 |
23 | assert.Nil(t, tmr.C())
24 |
25 | tmr.Reset(time.Millisecond)
26 |
27 | assert.NotNil(t, tmr.C())
28 | <-tmr.C()
29 |
30 | tmr.Clear()
31 |
32 | assert.Nil(t, tmr.C())
33 |
34 | tmr.Reset(time.Hour)
35 | tmr.Reset(time.Millisecond)
36 |
37 | <-tmr.C()
38 |
39 | tmr.Clear()
40 |
41 | tmr.Reset(time.Millisecond)
42 |
43 | time.Sleep(2 * time.Millisecond)
44 |
45 | tmr.Reset(0)
46 |
47 | tmr.Reset(time.Millisecond)
48 |
49 | time.Sleep(2 * time.Millisecond)
50 |
51 | tmr.Reset(time.Millisecond)
52 |
53 | <-tmr.C()
54 |
55 | tmr.Clear()
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/internal/containers/slice_set.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package containers
6 |
7 | import (
8 | "slices"
9 | )
10 |
11 | // SliceSet is a set implementation based on slices (for small number of items).
12 | type SliceSet[T comparable] struct {
13 | items []T
14 | }
15 |
16 | // Add item to the set.
17 | func (set *SliceSet[T]) Add(item T) bool {
18 | if slices.Contains(set.items, item) {
19 | return false
20 | }
21 |
22 | set.items = append(set.items, item)
23 |
24 | return true
25 | }
26 |
27 | // Contains returns true if the set contains the item.
28 | func (set *SliceSet[T]) Contains(item T) bool {
29 | return slices.Contains(set.items, item)
30 | }
31 |
32 | // Remove item from the set.
33 | func (set *SliceSet[T]) Remove(item T) (found bool) {
34 | idx := slices.Index(set.items, item)
35 | if idx == -1 {
36 | return false
37 | }
38 |
39 | set.items = slices.Delete(set.items, idx, idx+1)
40 |
41 | return true
42 | }
43 |
44 | // Len returns the number of items in the set.
45 | func (set *SliceSet[T]) Len() int {
46 | return len(set.items)
47 | }
48 |
--------------------------------------------------------------------------------
/.conform.yaml:
--------------------------------------------------------------------------------
1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
2 | #
3 | # Generated on 2024-02-28T13:50:02Z by kres latest.
4 |
5 | policies:
6 | - type: commit
7 | spec:
8 | dco: true
9 | gpg:
10 | required: true
11 | identity:
12 | gitHubOrganization: cosi-project
13 | spellcheck:
14 | locale: US
15 | maximumOfOneCommit: true
16 | header:
17 | length: 89
18 | imperative: true
19 | case: lower
20 | invalidLastCharacters: .
21 | body:
22 | required: true
23 | conventional:
24 | types:
25 | - chore
26 | - docs
27 | - perf
28 | - refactor
29 | - style
30 | - test
31 | - release
32 | scopes:
33 | - .*
34 | - type: license
35 | spec:
36 | root: .
37 | skipPaths:
38 | - .git/
39 | - testdata/
40 | includeSuffixes:
41 | - .go
42 | excludeSuffixes:
43 | - .pb.go
44 | - .pb.gw.go
45 | header: |
46 | // This Source Code Form is subject to the terms of the Mozilla Public
47 | // License, v. 2.0. If a copy of the MPL was not distributed with this
48 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
49 |
--------------------------------------------------------------------------------
/pkg/state/registry/namespace.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package registry
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/cosi-project/runtime/pkg/resource"
11 | "github.com/cosi-project/runtime/pkg/resource/meta"
12 | "github.com/cosi-project/runtime/pkg/state"
13 | )
14 |
15 | // NamespaceRegistry facilitates tracking namespaces.
16 | type NamespaceRegistry struct {
17 | state state.State
18 | }
19 |
20 | // NewNamespaceRegistry creates new NamespaceRegistry.
21 | func NewNamespaceRegistry(state state.State) *NamespaceRegistry {
22 | return &NamespaceRegistry{
23 | state: state,
24 | }
25 | }
26 |
27 | // RegisterDefault registers default namespaces.
28 | func (registry *NamespaceRegistry) RegisterDefault(ctx context.Context) error {
29 | return registry.Register(ctx, meta.NamespaceName, "Metadata namespace which contains resource and namespace definitions.")
30 | }
31 |
32 | // Register a namespace.
33 | func (registry *NamespaceRegistry) Register(ctx context.Context, ns resource.Namespace, description string) error {
34 | return registry.state.Create(ctx, meta.NewNamespace(ns, meta.NamespaceSpec{
35 | Description: description,
36 | }), state.WithCreateOwner(meta.Owner))
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/watch.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package qruntime
6 |
7 | import (
8 | "github.com/cosi-project/runtime/pkg/controller"
9 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/reduced"
10 | )
11 |
12 | // WatchTrigger is called by common controller runtime when there is a change in the watched resources.
13 | func (adapter *Adapter) WatchTrigger(md *reduced.Metadata) {
14 | // figure out the type: primary or mapped, and queue accordingly
15 | for _, in := range adapter.Inputs {
16 | if in.Namespace == md.Namespace && in.Type == md.Typ {
17 | switch in.Kind {
18 | case controller.InputQPrimary:
19 | item := NewQItemFromReduced(md, QJobReconcile)
20 | adapter.queue.Put(item.QKey, item.QValue)
21 | case controller.InputQMapped:
22 | item := NewQItemFromReduced(md, QJobMap)
23 | adapter.queue.Put(item.QKey, item.QValue)
24 | case controller.InputQMappedDestroyReady:
25 | if reduced.FilterDestroyReady(md) {
26 | item := NewQItemFromReduced(md, QJobMap)
27 | adapter.queue.Put(item.QKey, item.QValue)
28 | }
29 | }
30 | }
31 | }
32 |
33 | if adapter.queueLenExpVar != nil {
34 | adapter.queueLenExpVar.Set(adapter.queue.Len())
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/safe/writer.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package safe
6 |
7 | import (
8 | "context"
9 | "fmt"
10 |
11 | "github.com/cosi-project/runtime/pkg/controller"
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | )
14 |
15 | // WriterModify is a type safe wrapper around writer.Modify.
16 | func WriterModify[T resource.Resource](ctx context.Context, writer controller.Writer, r T, fn func(T) error, options ...controller.ModifyOption) error {
17 | return writer.Modify(ctx, r, func(r resource.Resource) error {
18 | arg, ok := r.(T)
19 | if !ok {
20 | return fmt.Errorf("type mismatch: expected %T, got %T", arg, r)
21 | }
22 |
23 | return fn(arg)
24 | }, options...)
25 | }
26 |
27 | // WriterModifyWithResult is a type safe wrapper around writer.ModifyWithResult.
28 | func WriterModifyWithResult[T resource.Resource](ctx context.Context, writer controller.Writer, r T, fn func(T) error, options ...controller.ModifyOption) (T, error) {
29 | got, err := writer.ModifyWithResult(ctx, r, func(r resource.Resource) error {
30 | arg, ok := r.(T)
31 | if !ok {
32 | return fmt.Errorf("type mismatch: expected %T, got %T", arg, r)
33 | }
34 |
35 | return fn(arg)
36 | }, options...)
37 |
38 | return typeAssertOrZero[T](got, err)
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/state/impl/inmem/backing_store.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package inmem
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/cosi-project/runtime/pkg/resource"
11 | )
12 |
13 | // LoadHandler is called for each resource loaded from the backing store.
14 | type LoadHandler func(resourceType resource.Type, resource resource.Resource) error
15 |
16 | // BackingStore provides a way to persist contents of in-memory resource collection.
17 | //
18 | // All resources are still kept in memory, but the backing store is used to persist
19 | // the resources across process restarts.
20 | //
21 | // BackingStore is responsible for marshaling/unmarshaling of resources.
22 | //
23 | // BackingStore is optional for in-memory resource collection.
24 | type BackingStore interface {
25 | // Load contents of the backing store into the in-memory resource collection.
26 | //
27 | // Handler should be called for each resource in the backing store.
28 | Load(ctx context.Context, handler LoadHandler) error
29 | // Put the resource to the backing store.
30 | Put(ctx context.Context, resourceType resource.Type, resource resource.Resource) error
31 | // Destroy the resource from the backing store.
32 | Destroy(ctx context.Context, resourceType resource.Type, resourcePointer resource.Pointer) error
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/internal/containers/slice_set_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package containers_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/qruntime/internal/containers"
14 | )
15 |
16 | func TestSliceSet(t *testing.T) {
17 | var set containers.SliceSet[int]
18 |
19 | for _, i := range []int{1, 2, 2, 3, 4, 5, 5} {
20 | set.Add(i)
21 | }
22 |
23 | assert.Equal(t, 5, set.Len())
24 |
25 | for _, i := range []int{1, 2, 3, 4, 5} {
26 | require.True(t, set.Contains(i))
27 | }
28 |
29 | require.False(t, set.Contains(0))
30 | require.False(t, set.Remove(0))
31 |
32 | for _, i := range []int{2, 4} {
33 | require.True(t, set.Remove(i))
34 | }
35 |
36 | assert.Equal(t, 3, set.Len())
37 |
38 | for _, i := range []int{1, 2, 2, 3, 4, 5, 5} {
39 | set.Add(i)
40 | }
41 |
42 | assert.Equal(t, 5, set.Len())
43 |
44 | for _, i := range []int{1, 2, 3, 4, 5} {
45 | require.True(t, set.Contains(i))
46 | }
47 |
48 | require.True(t, set.Remove(3))
49 | require.False(t, set.Remove(3))
50 |
51 | require.True(t, set.Add(3))
52 | require.False(t, set.Add(3))
53 |
54 | assert.Equal(t, 5, set.Len())
55 | }
56 |
--------------------------------------------------------------------------------
/pkg/state/registry/resource.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package registry
6 |
7 | import (
8 | "context"
9 | "fmt"
10 |
11 | "github.com/cosi-project/runtime/pkg/resource/meta"
12 | "github.com/cosi-project/runtime/pkg/state"
13 | )
14 |
15 | // ResourceRegistry facilitates tracking namespaces.
16 | type ResourceRegistry struct {
17 | state state.State
18 | }
19 |
20 | // NewResourceRegistry creates new ResourceRegistry.
21 | func NewResourceRegistry(state state.State) *ResourceRegistry {
22 | return &ResourceRegistry{
23 | state: state,
24 | }
25 | }
26 |
27 | // RegisterDefault registers default resource definitions.
28 | func (registry *ResourceRegistry) RegisterDefault(ctx context.Context) error {
29 | for _, r := range []meta.ResourceWithRD{&meta.ResourceDefinition{}, &meta.Namespace{}} {
30 | if err := registry.Register(ctx, r); err != nil {
31 | return err
32 | }
33 | }
34 |
35 | return nil
36 | }
37 |
38 | // Register a namespace.
39 | func (registry *ResourceRegistry) Register(ctx context.Context, r meta.ResourceWithRD) error {
40 | r, err := meta.NewResourceDefinition(r.ResourceDefinition())
41 | if err != nil {
42 | return fmt.Errorf("error registering resource %s: %w", r, err)
43 | }
44 |
45 | return registry.state.Create(ctx, r, state.WithCreateOwner(meta.Owner))
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/protobuf.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package store
6 |
7 | import (
8 | "github.com/cosi-project/runtime/api/v1alpha1"
9 | "github.com/cosi-project/runtime/pkg/resource"
10 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
11 | )
12 |
13 | // ProtobufMarshaler implements Marshaler using resources protobuf representation.
14 | //
15 | // Resources should implement protobuf marshaling.
16 | type ProtobufMarshaler struct{}
17 |
18 | // MarshalResource implements Marshaler interface.
19 | func (ProtobufMarshaler) MarshalResource(r resource.Resource) ([]byte, error) {
20 | protoR, err := protobuf.FromResource(r, protobuf.WithoutYAML())
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | protoD, err := protoR.Marshal()
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | return protobuf.ProtoMarshal(protoD)
31 | }
32 |
33 | // UnmarshalResource implements Marshaler interface.
34 | func (ProtobufMarshaler) UnmarshalResource(b []byte) (resource.Resource, error) { //nolint:ireturn
35 | var protoD v1alpha1.Resource
36 |
37 | if err := protobuf.ProtoUnmarshal(b, &protoD); err != nil {
38 | return nil, err
39 | }
40 |
41 | protoR, err := protobuf.Unmarshal(&protoD)
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | return protobuf.UnmarshalResource(protoR)
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/resource/rtestutils/ids.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package rtestutils
6 |
7 | import (
8 | "context"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/cosi-project/runtime/pkg/resource"
14 | "github.com/cosi-project/runtime/pkg/state"
15 | )
16 |
17 | // ResourceIDsWithOwner returns a list of resource IDs and filters them by owner (if set).
18 | func ResourceIDsWithOwner[R ResourceWithRD](ctx context.Context, t *testing.T, st state.State, owner *string, options ...state.ListOption) []resource.ID {
19 | require := require.New(t)
20 |
21 | var r R
22 |
23 | rds := r.ResourceDefinition()
24 |
25 | items, err := st.List(ctx, resource.NewMetadata(rds.DefaultNamespace, rds.Type, "", resource.VersionUndefined), options...)
26 | require.NoError(err)
27 |
28 | ids := make([]resource.ID, 0, len(items.Items))
29 |
30 | for _, item := range items.Items {
31 | if owner != nil && item.Metadata().Owner() != *owner {
32 | continue
33 | }
34 |
35 | ids = append(ids, item.Metadata().ID())
36 | }
37 |
38 | return ids
39 | }
40 |
41 | // ResourceIDs returns a list of resource IDs.
42 | func ResourceIDs[R ResourceWithRD](ctx context.Context, t *testing.T, st state.State, options ...state.ListOption) []resource.ID {
43 | return ResourceIDsWithOwner[R](ctx, t, st, nil, options...)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/reduced/reduced.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package reduced implements reducing resource metadata to a comparable value.
6 | package reduced
7 |
8 | import "github.com/cosi-project/runtime/pkg/resource"
9 |
10 | // Metadata reduces resource metadata for deduplication.
11 | //
12 | // It consists of two parts:
13 | // - a comparable Key which is used for deduplication.
14 | // - a Value which is reduced for duplicate keys to the last observed value.
15 | type Metadata struct {
16 | Key
17 | Value
18 | }
19 |
20 | // Key is a comparable representation of deduplication entry.
21 | type Key struct {
22 | Namespace resource.Namespace
23 | Typ resource.Type
24 | ID resource.ID
25 | }
26 |
27 | // Value is a reduced representation of resource metadata.
28 | type Value struct {
29 | Labels *resource.Labels
30 | Phase resource.Phase
31 | FinalizersEmpty bool
32 | }
33 |
34 | // NewMetadata creates a new reduced Metadata from a resource.Metadata.
35 | func NewMetadata(md *resource.Metadata) Metadata {
36 | return Metadata{
37 | Key: Key{
38 | Namespace: md.Namespace(),
39 | Typ: md.Type(),
40 | ID: md.ID(),
41 | },
42 | Value: Value{
43 | Phase: md.Phase(),
44 | FinalizersEmpty: md.Finalizers().Empty(),
45 | Labels: md.Labels(),
46 | },
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/resource/rtestutils/errors.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package rtestutils
6 |
7 | import (
8 | "fmt"
9 | "sort"
10 | "strings"
11 | )
12 |
13 | type assertionAggregator struct {
14 | errors map[string]struct{}
15 | hadErrors bool
16 | }
17 |
18 | func (agg *assertionAggregator) Errorf(format string, args ...any) {
19 | errorString := fmt.Sprintf(format, args...)
20 |
21 | if agg.errors == nil {
22 | agg.errors = map[string]struct{}{}
23 | }
24 |
25 | agg.errors[errorString] = struct{}{}
26 | agg.hadErrors = true
27 | }
28 |
29 | func (agg *assertionAggregator) String() string {
30 | lines := make([]string, 0, len(agg.errors))
31 |
32 | for errorString := range agg.errors {
33 | lines = append(lines, " * "+errorString)
34 | }
35 |
36 | sort.Strings(lines)
37 |
38 | return strings.Join(lines, "\n")
39 | }
40 |
41 | func (agg *assertionAggregator) Equal(other *assertionAggregator) bool {
42 | if agg.hadErrors != other.hadErrors {
43 | return false
44 | }
45 |
46 | if agg.errors == nil {
47 | return other.errors == nil
48 | }
49 |
50 | if other.errors == nil {
51 | return false
52 | }
53 |
54 | if len(agg.errors) != len(other.errors) {
55 | return false
56 | }
57 |
58 | for errorString := range agg.errors {
59 | if _, ok := other.errors[errorString]; !ok {
60 | return false
61 | }
62 | }
63 |
64 | return true
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/adapter/adapter.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package adapter provides common interface for controller adapters.
6 | package adapter
7 |
8 | import (
9 | "context"
10 |
11 | "go.uber.org/zap"
12 |
13 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/cache"
14 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/dependency"
15 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/reduced"
16 | "github.com/cosi-project/runtime/pkg/controller/runtime/options"
17 | "github.com/cosi-project/runtime/pkg/resource"
18 | "github.com/cosi-project/runtime/pkg/state"
19 | )
20 |
21 | // Adapter is common interface for controller adapters.
22 | type Adapter interface {
23 | // Run starts the adapter.
24 | Run(ctx context.Context)
25 | // WatchTrigger is called to notify the adapter about a new watch event.
26 | //
27 | // WatchTrigger should not block and should process the event asynchronously.
28 | WatchTrigger(md *reduced.Metadata)
29 | }
30 |
31 | // Options are options for creating a new Adapter.
32 | type Options struct {
33 | Logger *zap.Logger
34 | State state.State
35 | Cache *cache.ResourceCache
36 | DepDB *dependency.Database
37 | RegisterWatch func(resourceNamespace resource.Namespace, resourceType resource.Type) error
38 | RuntimeOptions options.Options
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/resource/any_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource_test
6 |
7 | import (
8 | "strings"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "go.yaml.in/yaml/v4"
13 |
14 | "github.com/cosi-project/runtime/pkg/resource"
15 | )
16 |
17 | type protoSpec struct{}
18 |
19 | func (s *protoSpec) GetYaml() []byte {
20 | return []byte(`value: xyz
21 | something: [a, b, c]
22 | `)
23 | }
24 |
25 | func TestNewAnyFromProto(t *testing.T) {
26 | r, err := resource.NewAnyFromProto(&protoMd{}, &protoSpec{})
27 | assert.NoError(t, err)
28 |
29 | assert.Equal(t, map[string]any{"something": []any{"a", "b", "c"}, "value": "xyz"}, r.Value())
30 | assert.Equal(t, "aaa", r.Metadata().ID())
31 |
32 | enc, err := resource.MarshalYAML(r)
33 | assert.NoError(t, err)
34 |
35 | out, err := yaml.Marshal(enc)
36 | assert.NoError(t, err)
37 |
38 | assert.Equal(t, strings.TrimSpace(`
39 | metadata:
40 | namespace: default
41 | type: type
42 | id: aaa
43 | version: 1
44 | owner: FooController
45 | phase: running
46 | created: 2021-06-23T19:22:29Z
47 | updated: 2021-06-23T19:22:29Z
48 | labels:
49 | app: foo
50 | stage: initial
51 | annotations:
52 | ttl: 1h
53 | finalizers:
54 | - resource1
55 | - resource2
56 | spec:
57 | value: xyz
58 | something: [a, b, c]
59 | `)+"\n", string(out))
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/internal/timer/resettable.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package timer provides a resettable timer.
6 | package timer
7 |
8 | import "time"
9 |
10 | // ResettableTimer wraps time.Timer to allow resetting the timer to any duration.
11 | type ResettableTimer struct {
12 | timer *time.Timer
13 | }
14 |
15 | // Reset resets the timer to the given duration.
16 | //
17 | // If the duration is zero, the timer is removed (and stopped as needed).
18 | // If the duration is non-zero, the timer is created if it doesn't exist, or reset if it does.
19 | func (rt *ResettableTimer) Reset(delay time.Duration) {
20 | if delay == 0 {
21 | if rt.timer != nil {
22 | if !rt.timer.Stop() {
23 | <-rt.timer.C
24 | }
25 |
26 | rt.timer = nil
27 | }
28 | } else {
29 | if rt.timer == nil {
30 | rt.timer = time.NewTimer(delay)
31 | } else {
32 | if !rt.timer.Stop() {
33 | <-rt.timer.C
34 | }
35 |
36 | rt.timer.Reset(delay)
37 | }
38 | }
39 | }
40 |
41 | // Clear should be called after receiving from the timer channel.
42 | func (rt *ResettableTimer) Clear() {
43 | rt.timer = nil
44 | }
45 |
46 | // C returns the timer channel.
47 | //
48 | // If the timer was not reset to a non-zero duration, nil is returned.
49 | func (rt *ResettableTimer) C() <-chan time.Time {
50 | if rt.timer == nil {
51 | return nil
52 | }
53 |
54 | return rt.timer.C
55 | }
56 |
--------------------------------------------------------------------------------
/pkg/resource/finalizer_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | )
14 |
15 | func TestFinalizers(t *testing.T) {
16 | const (
17 | A resource.Finalizer = "A"
18 | B resource.Finalizer = "B"
19 | C resource.Finalizer = "C"
20 | )
21 |
22 | var fins resource.Finalizers
23 |
24 | assert.True(t, fins.Empty())
25 |
26 | assert.True(t, fins.Add(A))
27 |
28 | finsCopy := fins
29 |
30 | assert.False(t, fins.Empty())
31 | assert.False(t, finsCopy.Empty())
32 |
33 | assert.True(t, fins.Add(B))
34 | assert.False(t, fins.Add(B))
35 | assert.True(t, fins.Has(B))
36 | assert.False(t, fins.Has(C))
37 |
38 | assert.True(t, finsCopy.Add(B))
39 |
40 | assert.False(t, fins.Remove(C))
41 | assert.True(t, fins.Remove(B))
42 | assert.False(t, fins.Remove(B))
43 | assert.False(t, fins.Has(B))
44 | assert.False(t, fins.Has(C))
45 |
46 | finsCopy = fins
47 |
48 | assert.True(t, finsCopy.Add(C))
49 | assert.True(t, fins.Add(C))
50 | assert.True(t, fins.Remove(C))
51 |
52 | fins = nil
53 |
54 | finsCopy.Set(fins)
55 | assert.True(t, finsCopy.Empty())
56 | assert.Nil(t, finsCopy)
57 |
58 | assert.True(t, fins.Add(A))
59 | assert.True(t, fins.Add(C))
60 |
61 | finsCopy.Set(fins)
62 | assert.True(t, finsCopy.Has(A))
63 | assert.True(t, finsCopy.Has(C))
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/resource/finalizer.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import "slices"
8 |
9 | // Finalizer is a free-form string which blocks resource destruction.
10 | //
11 | // Resource can't be destroyed until all the finalizers are cleared.
12 | type Finalizer = string
13 |
14 | // Finalizers is a set of Finalizer's with methods to add/remove items.
15 | //
16 | //nolint:recvcheck
17 | type Finalizers []Finalizer
18 |
19 | // Add a (unique) Finalizer to the set.
20 | func (fins *Finalizers) Add(fin Finalizer) bool {
21 | *fins = slices.Clone(*fins)
22 |
23 | if slices.Contains(*fins, fin) {
24 | return false
25 | }
26 |
27 | *fins = append(*fins, fin)
28 |
29 | return true
30 | }
31 |
32 | // Remove a (unique) Finalizer from the set.
33 | func (fins *Finalizers) Remove(fin Finalizer) bool {
34 | *fins = slices.Clone(*fins)
35 |
36 | for i, f := range *fins {
37 | if f == fin {
38 | *fins = append((*fins)[:i], (*fins)[i+1:]...)
39 |
40 | return true
41 | }
42 | }
43 |
44 | return false
45 | }
46 |
47 | // Empty returns true if list of finalizers is empty.
48 | func (fins Finalizers) Empty() bool {
49 | return len(fins) == 0
50 | }
51 |
52 | // Has returns true if fin is present in the list of finalizers.
53 | func (fins Finalizers) Has(fin Finalizer) bool {
54 | return slices.Contains(fins, fin)
55 | }
56 |
57 | // Set copies the finalizers from the other.
58 | func (fins *Finalizers) Set(other Finalizers) {
59 | *fins = slices.Clone(other)
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/bolt/bbolt.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package bolt implements inmem resource collection backing store in BoltDB (github.com/etcd-io/bbolt).
6 | package bolt
7 |
8 | import (
9 | "go.etcd.io/bbolt"
10 |
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | "github.com/cosi-project/runtime/pkg/state/impl/store"
13 | )
14 |
15 | // BackingStore implements inmem.BackingStore using BoltDB.
16 | //
17 | // Layout of the database:
18 | //
19 | // -> top-level bucket: $namespace
20 | // -> bucket: $resourceType
21 | // -> key: $resourceID
22 | // -> value: marshaled resource
23 | type BackingStore struct {
24 | db *bbolt.DB
25 | marshaler store.Marshaler
26 | }
27 |
28 | // NewBackingStore opens the BoltDB store with the given marshaler.
29 | func NewBackingStore(opener func() (*bbolt.DB, error), marshaler store.Marshaler) (*BackingStore, error) {
30 | db, err := opener()
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | return &BackingStore{
36 | db: db,
37 | marshaler: marshaler,
38 | }, nil
39 | }
40 |
41 | // Close the database.
42 | func (store *BackingStore) Close() error {
43 | return store.db.Close()
44 | }
45 |
46 | // WithNamespace returns an implementation of inmem.BackingStore interface for a given namespace.
47 | func (store *BackingStore) WithNamespace(namespace resource.Namespace) *NamespacedBackingStore {
48 | return &NamespacedBackingStore{
49 | store: store,
50 | namespace: namespace,
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/resource/annotations_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | )
14 |
15 | func TestAnnotations(t *testing.T) {
16 | var annotations resource.Annotations
17 |
18 | assert.True(t, annotations.Empty())
19 |
20 | annotations.Set("a", "b")
21 | assert.False(t, annotations.Empty())
22 |
23 | v, ok := annotations.Get("a")
24 | assert.True(t, ok)
25 | assert.Equal(t, "b", v)
26 |
27 | annotationsCopy := annotations
28 | annotations.Set("c", "d")
29 |
30 | assert.False(t, annotations.Equal(annotationsCopy))
31 |
32 | v, ok = annotations.Get("c")
33 | assert.True(t, ok)
34 | assert.Equal(t, "d", v)
35 |
36 | _, ok = annotationsCopy.Get("c")
37 | assert.False(t, ok)
38 |
39 | annotationsCopy2 := annotations
40 | annotationsCopy2.Set("a", "bb")
41 | assert.False(t, annotations.Equal(annotationsCopy2))
42 |
43 | annotationsCopy3 := annotations
44 | assert.True(t, annotations.Equal(annotationsCopy3))
45 |
46 | annotationsCopy3.Set("a", "b")
47 | assert.True(t, annotations.Equal(annotationsCopy3))
48 |
49 | annotationsCopy3.Delete("d")
50 | assert.True(t, annotations.Equal(annotationsCopy3))
51 |
52 | annotationsCopy3.Delete("a")
53 | assert.False(t, annotations.Equal(annotationsCopy3))
54 |
55 | _, ok = annotationsCopy3.Get("a")
56 | assert.False(t, ok)
57 |
58 | _, ok = annotations.Get("a")
59 | assert.True(t, ok)
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/resource/id_query_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource_test
6 |
7 | import (
8 | "regexp"
9 | "testing"
10 |
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | )
13 |
14 | func TestIDQuery(t *testing.T) {
15 | t.Parallel()
16 |
17 | for _, test := range []struct { //nolint:govet
18 | name string
19 | opts []resource.IDQueryOption
20 | id string
21 | want bool
22 | }{
23 | {
24 | name: "empty",
25 | opts: nil,
26 | id: "foo",
27 | want: true,
28 | },
29 | {
30 | name: "match",
31 | opts: []resource.IDQueryOption{
32 | resource.IDRegexpMatch(regexp.MustCompile("^first-")),
33 | },
34 | id: "first-second",
35 | want: true,
36 | },
37 | {
38 | name: "no match",
39 | opts: []resource.IDQueryOption{
40 | resource.IDRegexpMatch(regexp.MustCompile("^first-")),
41 | },
42 | id: "second-first-third",
43 | want: false,
44 | },
45 | {
46 | name: "match middle",
47 | opts: []resource.IDQueryOption{
48 | resource.IDRegexpMatch(regexp.MustCompile("first-")),
49 | },
50 | id: "second-first-third",
51 | want: true,
52 | },
53 | } {
54 | t.Run(test.name, func(t *testing.T) {
55 | t.Parallel()
56 |
57 | q := resource.IDQuery{}
58 |
59 | for _, o := range test.opts {
60 | o(&q)
61 | }
62 |
63 | if got := q.Matches(resource.NewMetadata("namespace", "type", test.id, resource.VersionUndefined)); got != test.want {
64 | t.Fatalf("unexpected result: got %t, want %t", got, test.want)
65 | }
66 | })
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/resource/tombstone.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import "fmt"
8 |
9 | var _ Resource = (*Tombstone)(nil)
10 |
11 | // Tombstone is a resource without a Spec.
12 | //
13 | // Tombstones are used to present state of a deleted resource.
14 | type Tombstone struct {
15 | ref Metadata
16 | }
17 |
18 | // NewTombstone builds a tombstone from resource reference.
19 | func NewTombstone(ref Reference) *Tombstone {
20 | return &Tombstone{
21 | ref: NewMetadata(ref.Namespace(), ref.Type(), ref.ID(), ref.Version()),
22 | }
23 | }
24 |
25 | // String method for debugging/logging.
26 | func (t *Tombstone) String() string {
27 | return fmt.Sprintf("Tombstone(%s)", t.ref.String())
28 | }
29 |
30 | // Metadata for the resource.
31 | //
32 | // Metadata.Version should change each time Spec changes.
33 | func (t *Tombstone) Metadata() *Metadata {
34 | return &t.ref
35 | }
36 |
37 | // Spec is not implemented for tobmstones.
38 | func (t *Tombstone) Spec() any {
39 | panic("tombstone doesn't contain spec")
40 | }
41 |
42 | // DeepCopy returns self, as tombstone is immutable.
43 | func (t *Tombstone) DeepCopy() Resource { //nolint:ireturn
44 | return t
45 | }
46 |
47 | // Tombstone implements Tombstoned interface.
48 | func (t *Tombstone) Tombstone() {
49 | }
50 |
51 | // Tombstoned is a marker interface for Tombstones.
52 | type Tombstoned interface {
53 | Tombstone()
54 | }
55 |
56 | // IsTombstone checks if resource is represented by the Tombstone.
57 | func IsTombstone(res Resource) bool {
58 | _, ok := res.(Tombstoned)
59 |
60 | return ok
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/state/conformance/resources.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package conformance
6 |
7 | import (
8 | "fmt"
9 |
10 | "github.com/cosi-project/runtime/pkg/resource"
11 | )
12 |
13 | // PathResourceType is the type of PathResource.
14 | const PathResourceType = resource.Type("os/path")
15 |
16 | // PathResource represents a path in the filesystem.
17 | //
18 | // Resource ID is the path.
19 | type PathResource struct {
20 | md resource.Metadata
21 | }
22 |
23 | type pathSpec struct{}
24 |
25 | func (spec pathSpec) MarshalProto() ([]byte, error) {
26 | return nil, nil
27 | }
28 |
29 | // NewPathResource creates new PathResource.
30 | func NewPathResource(ns resource.Namespace, path string) *PathResource {
31 | r := &PathResource{
32 | md: resource.NewMetadata(ns, PathResourceType, path, resource.VersionUndefined),
33 | }
34 |
35 | return r
36 | }
37 |
38 | // Metadata implements resource.Resource.
39 | func (path *PathResource) Metadata() *resource.Metadata {
40 | return &path.md
41 | }
42 |
43 | // Spec implements resource.Resource.
44 | func (path *PathResource) Spec() any {
45 | return pathSpec{}
46 | }
47 |
48 | // DeepCopy implements resource.Resource.
49 | func (path *PathResource) DeepCopy() resource.Resource { //nolint:ireturn
50 | return &PathResource{
51 | md: path.md,
52 | }
53 | }
54 |
55 | // UnmarshalProto implements protobuf.ResourceUnmarshaler.
56 | func (path *PathResource) UnmarshalProto(md *resource.Metadata, protoSpec []byte) error {
57 | path.md = *md
58 |
59 | if protoSpec != nil {
60 | return fmt.Errorf("unexpected non-nil protoSpec")
61 | }
62 |
63 | return nil
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/task/runner_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package task_test
6 |
7 | import (
8 | "testing"
9 | "time"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "go.uber.org/zap/zaptest"
13 |
14 | "github.com/cosi-project/runtime/pkg/task"
15 | )
16 |
17 | func TestRunner(t *testing.T) {
18 | t.Parallel()
19 |
20 | logger := zaptest.NewLogger(t)
21 | ctx := t.Context()
22 |
23 | assert := assert.New(t)
24 |
25 | in := &taskInputMock{
26 | commandCh: make(chan taskCommand),
27 | }
28 |
29 | assertTask := func(id string, expectedRunning bool) {
30 | assert.Eventually(func() bool {
31 | running, _ := in.runningTasks.Get(id)
32 |
33 | return running == expectedRunning
34 | }, time.Second, time.Millisecond)
35 | }
36 |
37 | runner := task.NewRunner(func(a, b taskSpec) bool {
38 | return a == b
39 | })
40 |
41 | runner.Reconcile(ctx, logger, nil, in)
42 |
43 | runner.Reconcile(ctx, logger, map[task.ID]taskSpec{
44 | "task1": "task1",
45 | "task2": "task2",
46 | }, in)
47 |
48 | assertTask("task1", true)
49 | assertTask("task2", true)
50 |
51 | runner.Reconcile(ctx, logger, map[task.ID]taskSpec{
52 | "task2": "task2",
53 | }, in)
54 |
55 | assertTask("task1", false)
56 | assertTask("task2", true)
57 |
58 | runner.Reconcile(ctx, logger, map[task.ID]taskSpec{
59 | "task2": "task3", // a bit of hack with different IDs to test the replace logic
60 | "task4": "task4",
61 | }, in)
62 |
63 | assertTask("task2", false)
64 | assertTask("task3", true)
65 | assertTask("task4", true)
66 |
67 | runner.Stop()
68 |
69 | assertTask("task3", false)
70 | assertTask("task4", false)
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/resource/protobuf/protobuf.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package protobuf provides a bridge between resources and protobuf interface.
6 | package protobuf
7 |
8 | import "google.golang.org/protobuf/proto"
9 |
10 | // vtprotoMessage is the interface for vtproto additions.
11 | //
12 | // We use only a subset of that interface but include additional methods
13 | // to prevent accidental successful type assertion for unrelated types.
14 | type vtprotoMessage interface {
15 | MarshalVT() ([]byte, error)
16 | MarshalToVT([]byte) (int, error)
17 | MarshalToSizedBufferVT([]byte) (int, error)
18 | UnmarshalVT([]byte) error
19 | }
20 |
21 | type vtprotoEqual interface {
22 | EqualMessageVT(proto.Message) bool
23 | }
24 |
25 | // ProtoMarshal returns the wire-format encoding of m.
26 | func ProtoMarshal(m proto.Message) ([]byte, error) {
27 | if vm, ok := m.(vtprotoMessage); ok {
28 | return vm.MarshalVT()
29 | }
30 |
31 | return proto.Marshal(m)
32 | }
33 |
34 | // ProtoUnmarshal parses the wire-format message in b and places the result in m.
35 | // The provided message must be mutable (e.g., a non-nil pointer to a message).
36 | func ProtoUnmarshal(b []byte, m proto.Message) error {
37 | if vm, ok := m.(vtprotoMessage); ok {
38 | return vm.UnmarshalVT(b)
39 | }
40 |
41 | return proto.Unmarshal(b, m)
42 | }
43 |
44 | // ProtoEqual returns true if the two messages are equal.
45 | //
46 | // This is a wrapper around proto.Equal which also supports vtproto messages.
47 | func ProtoEqual(a, b proto.Message) bool {
48 | if vm, ok := a.(vtprotoEqual); ok {
49 | return vm.EqualMessageVT(b)
50 | }
51 |
52 | return proto.Equal(a, b)
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/protobuf_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package store_test
6 |
7 | import (
8 | "strings"
9 | "testing"
10 |
11 | "github.com/siderolabs/gen/ensure"
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/require"
14 |
15 | "github.com/cosi-project/runtime/pkg/resource"
16 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
17 | "github.com/cosi-project/runtime/pkg/state/conformance"
18 | "github.com/cosi-project/runtime/pkg/state/impl/store"
19 | )
20 |
21 | func TestProtobufMarshaler(t *testing.T) {
22 | path := conformance.NewPathResource("default", "var/run")
23 | path.Metadata().Labels().Set("app", "foo")
24 | path.Metadata().Annotations().Set("ttl", "1h")
25 | path.Metadata().Finalizers().Add("controller1")
26 |
27 | marshaler := store.ProtobufMarshaler{}
28 |
29 | data, err := marshaler.MarshalResource(path)
30 | require.NoError(t, err)
31 |
32 | unmarshaled, err := marshaler.UnmarshalResource(data)
33 | require.NoError(t, err)
34 |
35 | assert.Equal(t, resource.String(path), resource.String(unmarshaled))
36 | }
37 |
38 | func BenchmarkProto(b *testing.B) {
39 | path := conformance.NewPathResource("default", strings.Repeat("var/run", 100))
40 | path.Metadata().Labels().Set("app", "foo")
41 | path.Metadata().Annotations().Set("ttl", "1h")
42 | path.Metadata().Finalizers().Add("controller1")
43 |
44 | marshaler := store.ProtobufMarshaler{}
45 |
46 | for range b.N {
47 | _, err := marshaler.MarshalResource(path)
48 | require.NoError(b, err)
49 | }
50 | }
51 |
52 | func init() {
53 | ensure.NoError(protobuf.RegisterResource(conformance.PathResourceType, &conformance.PathResource{}))
54 | }
55 |
--------------------------------------------------------------------------------
/pkg/resource/version.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import (
8 | "fmt"
9 | "strconv"
10 |
11 | "github.com/siderolabs/go-pointer"
12 | )
13 |
14 | // Version of a resource.
15 | type Version struct {
16 | // make versions uncomparable with equality operator
17 | _ [0]func()
18 |
19 | *uint64
20 | }
21 |
22 | // Special version constants.
23 | var (
24 | VersionUndefined = Version{}
25 | )
26 |
27 | const undefinedVersion = "undefined"
28 |
29 | // Value returns the underlying version number. Returns 0 if the version is undefined.
30 | func (v Version) Value() uint64 {
31 | return pointer.SafeDeref(v.uint64)
32 | }
33 |
34 | // Next returns a new incremented version.
35 | func (v Version) Next() Version {
36 | return Version{
37 | uint64: pointer.To(pointer.SafeDeref(v.uint64) + 1),
38 | }
39 | }
40 |
41 | func (v Version) String() string {
42 | if v.uint64 == nil {
43 | return undefinedVersion
44 | }
45 |
46 | return strconv.FormatUint(*v.uint64, 10)
47 | }
48 |
49 | // Equal compares versions.
50 | func (v Version) Equal(other Version) bool {
51 | if v.uint64 == nil || other.uint64 == nil {
52 | return v.uint64 == nil && other.uint64 == nil
53 | }
54 |
55 | return *v.uint64 == *other.uint64
56 | }
57 |
58 | // ParseVersion from string representation.
59 | func ParseVersion(ver string) (Version, error) {
60 | if ver == undefinedVersion {
61 | return VersionUndefined, nil
62 | }
63 |
64 | intVersion, err := strconv.ParseInt(ver, 10, 64)
65 | if err != nil {
66 | return VersionUndefined, fmt.Errorf("error parsing version: %w", err)
67 | }
68 |
69 | return Version{
70 | uint64: pointer.To(uint64(intVersion)),
71 | }, nil
72 | }
73 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cosi-project/runtime
2 |
3 | go 1.24.0
4 |
5 | require (
6 | github.com/ProtonMail/gopenpgp/v2 v2.9.0
7 | github.com/cenkalti/backoff/v4 v4.3.0
8 | github.com/gertd/go-pluralize v0.2.1
9 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3
10 | github.com/hashicorp/go-multierror v1.1.1
11 | github.com/klauspost/compress v1.18.1
12 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10
13 | github.com/siderolabs/gen v0.8.5
14 | github.com/siderolabs/go-pointer v1.0.1
15 | github.com/siderolabs/go-retry v0.3.3
16 | github.com/siderolabs/protoenc v0.2.4
17 | github.com/stretchr/testify v1.11.1
18 | go.etcd.io/bbolt v1.4.3
19 | go.uber.org/goleak v1.3.0
20 | go.uber.org/zap v1.27.0
21 | go.yaml.in/yaml/v4 v4.0.0-rc.2
22 | golang.org/x/sync v0.17.0
23 | golang.org/x/time v0.14.0
24 | google.golang.org/grpc v1.76.0
25 | google.golang.org/protobuf v1.36.10
26 | )
27 |
28 | require (
29 | github.com/ProtonMail/go-crypto v1.3.0 // indirect
30 | github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
31 | github.com/cloudflare/circl v1.6.1 // indirect
32 | github.com/davecgh/go-spew v1.1.1 // indirect
33 | github.com/hashicorp/errwrap v1.1.0 // indirect
34 | github.com/pkg/errors v0.9.1 // indirect
35 | github.com/pmezard/go-difflib v1.0.0 // indirect
36 | github.com/rogpeppe/go-internal v1.14.1 // indirect
37 | go.uber.org/multierr v1.11.0 // indirect
38 | golang.org/x/crypto v0.40.0 // indirect
39 | golang.org/x/net v0.42.0 // indirect
40 | golang.org/x/sys v0.34.0 // indirect
41 | golang.org/x/text v0.29.0 // indirect
42 | google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
43 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
44 | gopkg.in/yaml.v3 v3.0.1 // indirect
45 | )
46 |
47 | retract (
48 | v0.7.3 // Typo in the test type result
49 | v0.4.7 // Wait with locked mutex leads to the deadlock
50 | )
51 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/rruntime/run.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package rruntime
6 |
7 | import (
8 | "context"
9 | "errors"
10 | "fmt"
11 | "runtime/debug"
12 | "time"
13 |
14 | "go.uber.org/zap"
15 |
16 | "github.com/cosi-project/runtime/pkg/controller/runtime/metrics"
17 | "github.com/cosi-project/runtime/pkg/logging"
18 | )
19 |
20 | // Run the controller loop via the adapter.
21 | func (adapter *Adapter) Run(ctx context.Context) {
22 | logger := adapter.logger.With(logging.Controller(adapter.Name))
23 |
24 | for {
25 | err := adapter.runOnce(ctx, logger)
26 | if err == nil {
27 | return
28 | }
29 |
30 | if adapter.runtimeOptions.MetricsEnabled {
31 | metrics.ControllerCrashes.Add(adapter.Name, 1)
32 | }
33 |
34 | interval := adapter.backoff.NextBackOff()
35 |
36 | logger.Sugar().Debugf("restarting controller in %s", interval)
37 |
38 | select {
39 | case <-ctx.Done():
40 | return
41 | case <-time.After(interval):
42 | }
43 |
44 | // schedule reconcile after restart
45 | adapter.triggerReconcile()
46 | }
47 | }
48 |
49 | func (adapter *Adapter) runOnce(ctx context.Context, logger *zap.Logger) (err error) {
50 | defer func() {
51 | if err != nil && errors.Is(err, context.Canceled) {
52 | err = nil
53 | }
54 |
55 | if err != nil {
56 | logger.Error("controller failed", zap.Error(err))
57 | } else {
58 | logger.Debug("controller finished")
59 | }
60 |
61 | // clean up output tracker on any exit from Run method
62 | adapter.outputTracker = nil
63 | }()
64 |
65 | defer func() {
66 | if p := recover(); p != nil {
67 | err = fmt.Errorf("controller %q panicked: %s\n\n%s", adapter.Name, p, string(debug.Stack()))
68 | }
69 | }()
70 |
71 | logger.Debug("controller starting")
72 |
73 | return adapter.ctrl.Run(ctx, adapter, logger)
74 | }
75 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/bolt/conformance_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package bolt_test
6 |
7 | import (
8 | "path/filepath"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | "github.com/stretchr/testify/suite"
14 | "go.etcd.io/bbolt"
15 |
16 | "github.com/cosi-project/runtime/pkg/resource"
17 | "github.com/cosi-project/runtime/pkg/state"
18 | "github.com/cosi-project/runtime/pkg/state/conformance"
19 | "github.com/cosi-project/runtime/pkg/state/impl/inmem"
20 | "github.com/cosi-project/runtime/pkg/state/impl/namespaced"
21 | "github.com/cosi-project/runtime/pkg/state/impl/store"
22 | "github.com/cosi-project/runtime/pkg/state/impl/store/bolt"
23 | "github.com/cosi-project/runtime/pkg/state/impl/store/encryption"
24 | )
25 |
26 | func TestBboltConformance(t *testing.T) {
27 | t.Parallel()
28 |
29 | tmpDir := t.TempDir()
30 |
31 | marshaler := encryption.NewMarshaler(
32 | store.ProtobufMarshaler{},
33 | encryption.NewCipher(
34 | encryption.KeyProviderFunc(func() ([]byte, error) {
35 | return []byte("this key len is exactly 32 bytes"), nil
36 | }),
37 | ),
38 | )
39 |
40 | backingStore, err := bolt.NewBackingStore(
41 | func() (*bbolt.DB, error) {
42 | return bbolt.Open(filepath.Join(tmpDir, "test.db"), 0o600, nil)
43 | },
44 | marshaler,
45 | )
46 | require.NoError(t, err)
47 |
48 | t.Cleanup(func() {
49 | assert.NoError(t, backingStore.Close())
50 | })
51 |
52 | suite.Run(t, &conformance.StateSuite{
53 | State: state.WrapCore(namespaced.NewState(
54 | func(ns resource.Namespace) state.CoreState {
55 | return inmem.NewStateWithOptions(
56 | inmem.WithBackingStore(backingStore.WithNamespace(ns)),
57 | )(ns)
58 | },
59 | )),
60 | Namespaces: []resource.Namespace{"default", "controller", "system", "runtime"},
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/pkg/resource/resource.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package resource provides core resource definition.
6 | package resource
7 |
8 | import (
9 | "fmt"
10 | "reflect"
11 | )
12 |
13 | type (
14 | // ID is a resource ID.
15 | ID = string
16 | // Type is a resource type.
17 | //
18 | // Type could be e.g. runtime/os/mount.
19 | Type = string
20 | // Namespace of a resource.
21 | Namespace = string
22 | )
23 |
24 | var _ Resource = (*Any)(nil)
25 |
26 | // Resource is an abstract resource managed by the state.
27 | //
28 | // Resource is uniquely identified by the tuple (Namespace, Type, ID).
29 | // Resource might have additional opaque data in Spec().
30 | // When resource is updated, Version should change with each update.
31 | type Resource interface {
32 | // Metadata for the resource.
33 | //
34 | // Metadata.Version should change each time Spec changes.
35 | Metadata() *Metadata
36 |
37 | // Opaque data resource contains.
38 | Spec() any
39 |
40 | // Deep copy of the resource.
41 | DeepCopy() Resource
42 | }
43 |
44 | // Equal tests two resources for equality.
45 | func Equal(r1, r2 Resource) bool {
46 | if !r1.Metadata().Equal(*r2.Metadata()) {
47 | return false
48 | }
49 |
50 | spec1, spec2 := r1.Spec(), r2.Spec()
51 |
52 | if equality, ok := spec1.(interface {
53 | Equal(any) bool
54 | }); ok {
55 | return equality.Equal(spec2)
56 | }
57 |
58 | return reflect.DeepEqual(spec1, spec2)
59 | }
60 |
61 | // MarshalYAML marshals resource to YAML definition.
62 | func MarshalYAML(r Resource) (any, error) {
63 | return &struct {
64 | Metadata *Metadata `yaml:"metadata"`
65 | Spec any `yaml:"spec"`
66 | }{
67 | Metadata: r.Metadata(),
68 | Spec: r.Spec(),
69 | }, nil
70 | }
71 |
72 | // String returns representation suitable for %s formatting.
73 | func String(r Resource) string {
74 | md := r.Metadata()
75 |
76 | return fmt.Sprintf("%s(%s/%s)", md.Type(), md.Namespace(), md.ID())
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/resource/any.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import (
8 | "errors"
9 |
10 | "go.yaml.in/yaml/v4"
11 | )
12 |
13 | // Any can hold data from any resource type.
14 | type Any struct {
15 | spec anySpec
16 | md Metadata
17 | }
18 |
19 | type anySpec struct {
20 | value any
21 | yaml []byte
22 | }
23 |
24 | // MarshalYAML implements yaml.Marshaler interface.
25 | func (s anySpec) MarshalYAML() (any, error) {
26 | var node yaml.Node
27 |
28 | if err := yaml.Unmarshal(s.yaml, &node); err != nil {
29 | return nil, err
30 | }
31 |
32 | // the `node` is expected to be a document node
33 | if node.Kind != yaml.DocumentNode || len(node.Content) == 0 {
34 | return nil, errors.New("invalid YAML content")
35 | }
36 |
37 | return &node.Content[0], nil
38 | }
39 |
40 | // SpecProto is a protobuf interface of resource spec.
41 | type SpecProto interface {
42 | GetYaml() []byte
43 | }
44 |
45 | // NewAnyFromProto unmarshals Any from protobuf interface.
46 | func NewAnyFromProto(protoMd MetadataProto, protoSpec SpecProto) (*Any, error) {
47 | md, err := NewMetadataFromProto(protoMd)
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | result := &Any{
53 | md: md,
54 | spec: anySpec{
55 | yaml: protoSpec.GetYaml(),
56 | },
57 | }
58 |
59 | if err = yaml.Unmarshal(result.spec.yaml, &result.spec.value); err != nil {
60 | return nil, err
61 | }
62 |
63 | return result, nil
64 | }
65 |
66 | // Metadata implements resource.Resource.
67 | func (a *Any) Metadata() *Metadata {
68 | return &a.md
69 | }
70 |
71 | // Spec implements resource.Resource.
72 | func (a *Any) Spec() any {
73 | return a.spec
74 | }
75 |
76 | // Value returns decoded value as Go type.
77 | func (a *Any) Value() any {
78 | return a.spec.value
79 | }
80 |
81 | // DeepCopy implements resource.Resource.
82 | func (a *Any) DeepCopy() Resource { //nolint:ireturn
83 | return &Any{
84 | md: a.md,
85 | spec: a.spec,
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/hack/govulncheck.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Source: https://github.com/tianon/gosu/blob/e157efb/govulncheck-with-excludes.sh
3 | # Licensed under the Apache License, Version 2.0
4 | # Copyright Tianon Gravi
5 | set -Eeuo pipefail
6 |
7 | exclude_arg=""
8 | pass_args=()
9 |
10 | while [[ $# -gt 0 ]]; do
11 | case "$1" in
12 | -exclude)
13 | exclude_arg="$2"
14 | shift 2
15 | ;;
16 | *)
17 | pass_args+=("$1")
18 | shift
19 | ;;
20 | esac
21 | done
22 |
23 | if [[ -n "$exclude_arg" ]]; then
24 | excludeVulns="$(jq -nc --arg list "$exclude_arg" '$list | split(",")')"
25 | else
26 | excludeVulns="[]"
27 | fi
28 |
29 | export excludeVulns
30 |
31 | # Debug print
32 | echo "excludeVulns = $excludeVulns"
33 | echo "Passing args: ${pass_args[*]}"
34 |
35 | if ! command -v govulncheck > /dev/null; then
36 | printf "govulncheck not installed"
37 | exit 1
38 | fi
39 |
40 | if out="$(govulncheck "${pass_args[@]}")"; then
41 | printf '%s\n' "$out"
42 | exit 0
43 | fi
44 |
45 | json="$(govulncheck -json "${pass_args[@]}")"
46 |
47 | vulns="$(jq <<<"$json" -cs '
48 | (
49 | map(
50 | .osv // empty
51 | | { key: .id, value: . }
52 | )
53 | | from_entries
54 | ) as $meta
55 | # https://github.com/tianon/gosu/issues/144
56 | | map(
57 | .finding // empty
58 | # https://github.com/golang/vuln/blob/3740f5cb12a3f93b18dbe200c4bcb6256f8586e2/internal/scan/template.go#L97-L104
59 | | select((.trace[0].function // "") != "")
60 | | .osv
61 | )
62 | | unique
63 | | map($meta[.])
64 | ')"
65 | if [ "$(jq <<<"$vulns" -r 'length')" -le 0 ]; then
66 | printf '%s\n' "$out"
67 | exit 1
68 | fi
69 |
70 | filtered="$(jq <<<"$vulns" -c '
71 | (env.excludeVulns | fromjson) as $exclude
72 | | map(select(
73 | .id as $id
74 | | $exclude | index($id) | not
75 | ))
76 | ')"
77 |
78 | text="$(jq <<<"$filtered" -r 'map("- \(.id) (aka \(.aliases | join(", ")))\n\n\t\(.details | gsub("\n"; "\n\t"))") | join("\n\n")')"
79 |
80 | if [ -z "$text" ]; then
81 | printf 'No vulnerabilities found.\n'
82 | exit 0
83 | else
84 | printf '%s\n' "$text"
85 | exit 1
86 | fi
87 |
--------------------------------------------------------------------------------
/pkg/controller/conformance/resource.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package conformance
6 |
7 | import "github.com/cosi-project/runtime/pkg/resource"
8 |
9 | // Resource represents some T value.
10 | type Resource[T any, S Spec[T], SS SpecPtr[T, S]] struct {
11 | value S
12 | md resource.Metadata
13 | }
14 |
15 | // NewResource creates new Resource.
16 | func NewResource[T any, S Spec[T], SS SpecPtr[T, S]](md resource.Metadata, value T) *Resource[T, S, SS] {
17 | var s S
18 |
19 | ss := SS(&s)
20 | ss.SetValue(value)
21 |
22 | r := &Resource[T, S, SS]{
23 | md: md,
24 | value: s,
25 | }
26 |
27 | return r
28 | }
29 |
30 | // Metadata implements resource.Resource.
31 | func (r *Resource[T, S, SS]) Metadata() *resource.Metadata {
32 | return &r.md
33 | }
34 |
35 | // Spec implements resource.Resource.
36 | func (r *Resource[T, S, SS]) Spec() any {
37 | return r.value
38 | }
39 |
40 | // Value returns a value inside the spec.
41 | func (r *Resource[T, S, SS]) Value() T { //nolint:ireturn
42 | return r.value.Value()
43 | }
44 |
45 | // SetValue set spec with provided value.
46 | func (r *Resource[T, S, SS]) SetValue(v T) {
47 | val := SS(&r.value)
48 | val.SetValue(v)
49 | }
50 |
51 | // DeepCopy implements resource.Resource.
52 | func (r *Resource[T, S, SS]) DeepCopy() resource.Resource { //nolint:ireturn
53 | return &Resource[T, S, SS]{
54 | md: r.md,
55 | value: r.value,
56 | }
57 | }
58 |
59 | // UnmarshalProto implements protobuf.ResourceUnmarshaler.
60 | func (r *Resource[T, S, SS]) UnmarshalProto(md *resource.Metadata, protoSpec []byte) error {
61 | r.md = *md
62 | val := SS(&r.value)
63 | val.FromProto(protoSpec)
64 |
65 | return nil
66 | }
67 |
68 | // SpecPtr requires Spec to be a pointer and have a set of methods.
69 | type SpecPtr[T, S any] interface {
70 | *S
71 | Spec[T]
72 | FromProto([]byte)
73 | SetValue(T)
74 | }
75 |
76 | // Spec requires spec to have a set of Get methods.
77 | type Spec[T any] interface {
78 | Value() T
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/resource/protobuf/registry_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package protobuf_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/siderolabs/gen/ensure"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 |
14 | "github.com/cosi-project/runtime/api/v1alpha1"
15 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
16 | "github.com/cosi-project/runtime/pkg/state/conformance"
17 | )
18 |
19 | func init() {
20 | ensure.NoError(protobuf.RegisterResource(conformance.PathResourceType, &conformance.PathResource{}))
21 | }
22 |
23 | func BenchmarkCreateResource(b *testing.B) {
24 | protoR := &v1alpha1.Resource{
25 | Metadata: &v1alpha1.Metadata{
26 | Namespace: "ns",
27 | Type: conformance.PathResourceType,
28 | Id: "a/b",
29 | Version: "3",
30 | Phase: "running",
31 | },
32 | Spec: &v1alpha1.Spec{
33 | YamlSpec: "nil",
34 | ProtoSpec: nil,
35 | },
36 | }
37 |
38 | r, err := protobuf.Unmarshal(protoR)
39 | require.NoError(b, err)
40 |
41 | b.ResetTimer()
42 |
43 | for range b.N {
44 | rr, err := protobuf.UnmarshalResource(r)
45 |
46 | if _, ok := rr.(*conformance.PathResource); !ok {
47 | b.Fatalf("unexpected resource type %T", rr)
48 | }
49 |
50 | if err != nil {
51 | b.Fatal(err)
52 | }
53 | }
54 | }
55 |
56 | func TestRegistry(t *testing.T) {
57 | t.Parallel()
58 |
59 | protoR := &v1alpha1.Resource{
60 | Metadata: &v1alpha1.Metadata{
61 | Namespace: "ns",
62 | Type: conformance.PathResourceType,
63 | Id: "a/b",
64 | Version: "3",
65 | Phase: "running",
66 | },
67 | Spec: &v1alpha1.Spec{
68 | YamlSpec: "nil",
69 | ProtoSpec: nil,
70 | },
71 | }
72 |
73 | r, err := protobuf.Unmarshal(protoR)
74 | require.NoError(t, err)
75 |
76 | rr, err := protobuf.UnmarshalResource(r)
77 | require.NoError(t, err)
78 |
79 | require.IsType(t, rr, &conformance.PathResource{})
80 |
81 | assert.Equal(t, rr.Metadata().ID(), "a/b")
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/rruntime/output_tracker.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package rruntime
6 |
7 | import (
8 | "context"
9 | "fmt"
10 |
11 | "github.com/siderolabs/gen/pair/ordered"
12 |
13 | "github.com/cosi-project/runtime/pkg/resource"
14 | )
15 |
16 | type outputTrackingID = ordered.Triple[resource.Namespace, resource.Type, resource.ID]
17 |
18 | func makeOutputTrackingID(md *resource.Metadata) outputTrackingID {
19 | return ordered.MakeTriple(md.Namespace(), md.Type(), md.ID())
20 | }
21 |
22 | // StartTrackingOutputs enables output tracking for the controller.
23 | func (adapter *Adapter) StartTrackingOutputs() {
24 | if adapter.outputTracker != nil {
25 | panic("output tracking already enabled")
26 | }
27 |
28 | adapter.outputTracker = trackingPoolInstance.Get()
29 | }
30 |
31 | // CleanupOutputs destroys all output resources that were not tracked.
32 | func (adapter *Adapter) CleanupOutputs(ctx context.Context, outputs ...resource.Kind) error {
33 | if adapter.outputTracker == nil {
34 | panic("output tracking not enabled")
35 | }
36 |
37 | for _, outputKind := range outputs {
38 | list, err := adapter.List(ctx, outputKind)
39 | if err != nil {
40 | return fmt.Errorf("error listing output resources: %w", err)
41 | }
42 |
43 | for _, resource := range list.Items {
44 | if resource.Metadata().Owner() != adapter.Name {
45 | // skip resources not owned by this controller
46 | continue
47 | }
48 |
49 | trackingID := makeOutputTrackingID(resource.Metadata())
50 |
51 | if _, touched := adapter.outputTracker[trackingID]; touched {
52 | // skip touched resources
53 | continue
54 | }
55 |
56 | if err = adapter.Destroy(ctx, resource.Metadata()); err != nil {
57 | return fmt.Errorf("error destroying resource %s: %w", resource.Metadata(), err)
58 | }
59 | }
60 | }
61 |
62 | trackingPoolInstance.Put(adapter.outputTracker)
63 | adapter.outputTracker = nil
64 |
65 | adapter.ResetRestartBackoff()
66 |
67 | return nil
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/resource/protobuf/spec_unmarshal_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package protobuf_test
6 |
7 | import (
8 | "encoding/json"
9 | "strings"
10 | "testing"
11 |
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/require"
14 | "go.yaml.in/yaml/v4"
15 | "google.golang.org/protobuf/reflect/protoreflect"
16 |
17 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
18 | )
19 |
20 | type rawSpec struct {
21 | Str string
22 | Num int
23 | }
24 |
25 | type customUnmarshalerSpec struct {
26 | Str string
27 | Num int
28 | }
29 |
30 | func (spec *customUnmarshalerSpec) ProtoReflect() protoreflect.Message { return nil }
31 |
32 | // UnmarshalJSON uppercases the string and doubles the number.
33 | func (spec *customUnmarshalerSpec) UnmarshalJSON(data []byte) error {
34 | var raw rawSpec
35 |
36 | err := json.Unmarshal(data, &raw)
37 | if err != nil {
38 | return err
39 | }
40 |
41 | spec.Str = strings.ToUpper(raw.Str)
42 | spec.Num = raw.Num * 2
43 |
44 | return nil
45 | }
46 |
47 | // UnmarshalYAML lowercases the string and halves the number.
48 | func (spec *customUnmarshalerSpec) UnmarshalYAML(node *yaml.Node) error {
49 | var raw rawSpec
50 |
51 | err := node.Decode(&raw)
52 | if err != nil {
53 | return err
54 | }
55 |
56 | spec.Str = strings.ToLower(raw.Str)
57 | spec.Num = raw.Num / 2
58 |
59 | return nil
60 | }
61 |
62 | func TestCustomJSONUnmarshal(t *testing.T) {
63 | spec := protobuf.NewResourceSpec(&customUnmarshalerSpec{})
64 |
65 | err := json.Unmarshal([]byte(`{"str":"aaaa","num":2222}`), &spec)
66 | require.NoError(t, err)
67 |
68 | assert.Equal(t, "AAAA", spec.Value.Str)
69 | assert.Equal(t, 4444, spec.Value.Num)
70 | }
71 |
72 | func TestCustomYAMLUnmarshal(t *testing.T) {
73 | spec := protobuf.NewResourceSpec(&customUnmarshalerSpec{})
74 |
75 | err := yaml.Unmarshal([]byte(`str: AAAA
76 | num: 2222`), &spec)
77 | require.NoError(t, err)
78 |
79 | assert.Equal(t, "aaaa", spec.Value.Str)
80 | assert.Equal(t, 1111, spec.Value.Num)
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/resource/internal/compare/compare.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package compare implements term operation helpers.
6 | package compare
7 |
8 | import (
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | // GetNumbers returns numbers parsed from left and right params, if any of string to number conversion fails returns false as the 3rd arg.
14 | func GetNumbers(left, right string) (int64, int64, bool) {
15 | numLeft, ok := parseValue(left)
16 | if !ok {
17 | return 0, 0, false
18 | }
19 |
20 | numRight, ok := parseValue(right)
21 | if !ok {
22 | return 0, 0, false
23 | }
24 |
25 | return numLeft, numRight, true
26 | }
27 |
28 | func parseValue(value string) (int64, bool) {
29 | value = strings.TrimSpace(value)
30 |
31 | splitPoint := len(value)
32 |
33 | for i, c := range value {
34 | if c >= '0' && c <= '9' || c == '-' {
35 | continue
36 | }
37 |
38 | splitPoint = i
39 |
40 | break
41 | }
42 |
43 | digits, units := value[:splitPoint], value[splitPoint:]
44 |
45 | if len(digits) == 0 {
46 | return 0, false
47 | }
48 |
49 | res, err := strconv.ParseInt(digits, 10, 64)
50 | if err != nil {
51 | return 0, false
52 | }
53 |
54 | multiplier, ok := getMultiplier(units)
55 | if !ok {
56 | return 0, false
57 | }
58 |
59 | return res * multiplier, true
60 | }
61 |
62 | func getMultiplier(value string) (int64, bool) {
63 | value = strings.TrimSpace(strings.ToLower(value))
64 |
65 | if len(value) == 0 {
66 | return 1, true
67 | }
68 |
69 | if len(value) > 1 {
70 | switch strings.ToLower(value[:2]) {
71 | case "pi":
72 | return 1 << 50, true
73 | case "ti":
74 | return 1 << 40, true
75 | case "gi":
76 | return 1 << 30, true
77 | case "mi":
78 | return 1 << 20, true
79 | case "ki":
80 | return 1 << 10, true
81 | }
82 | }
83 |
84 | switch strings.ToLower(value[:1]) {
85 | case "p":
86 | return 1e15, true
87 | case "t":
88 | return 1e12, true
89 | case "g":
90 | return 1e9, true
91 | case "m":
92 | return 1e6, true
93 | case "k":
94 | return 1e3, true
95 | }
96 |
97 | return 0, false
98 | }
99 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/rruntime/watch.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package rruntime
6 |
7 | import (
8 | "github.com/cosi-project/runtime/pkg/controller"
9 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/reduced"
10 | "github.com/cosi-project/runtime/pkg/controller/runtime/metrics"
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | )
13 |
14 | type watchKey struct {
15 | Namespace resource.Namespace
16 | Type resource.Type
17 | }
18 |
19 | func (adapter *Adapter) addWatchFilter(resourceNamespace resource.Namespace, resourceType resource.Type, filter reduced.WatchFilter) {
20 | adapter.watchFilterMu.Lock()
21 | defer adapter.watchFilterMu.Unlock()
22 |
23 | if adapter.watchFilters == nil {
24 | adapter.watchFilters = make(map[watchKey]reduced.WatchFilter)
25 | }
26 |
27 | adapter.watchFilters[watchKey{resourceNamespace, resourceType}] = filter
28 | }
29 |
30 | func (adapter *Adapter) deleteWatchFilter(resourceNamespace resource.Namespace, resourceType resource.Type) {
31 | adapter.watchFilterMu.Lock()
32 | defer adapter.watchFilterMu.Unlock()
33 |
34 | delete(adapter.watchFilters, watchKey{resourceNamespace, resourceType})
35 | }
36 |
37 | // WatchTrigger is called by common controller runtime when there is a change in the watched resources.
38 | func (adapter *Adapter) WatchTrigger(md *reduced.Metadata) {
39 | adapter.watchFilterMu.Lock()
40 | defer adapter.watchFilterMu.Unlock()
41 |
42 | if adapter.watchFilters != nil {
43 | if filter := adapter.watchFilters[watchKey{md.Namespace, md.Typ}]; filter != nil && !filter(md) {
44 | // skip reconcile if the event doesn't match the filter
45 | return
46 | }
47 | }
48 |
49 | adapter.triggerReconcile()
50 | }
51 |
52 | func (adapter *Adapter) triggerReconcile() {
53 | // schedule reconcile if channel is empty
54 | // otherwise channel is not empty, and reconcile is anyway scheduled
55 | select {
56 | case adapter.ch <- controller.ReconcileEvent{}:
57 | if adapter.runtimeOptions.MetricsEnabled {
58 | metrics.ControllerWakeups.Add(adapter.Name, 1)
59 | }
60 |
61 | default:
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/task/task_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package task_test
6 |
7 | import (
8 | "context"
9 | "errors"
10 | "testing"
11 | "time"
12 |
13 | "github.com/siderolabs/gen/containers"
14 | "github.com/stretchr/testify/assert"
15 | "go.uber.org/zap"
16 | "go.uber.org/zap/zaptest"
17 |
18 | "github.com/cosi-project/runtime/pkg/task"
19 | )
20 |
21 | type taskCommand struct {
22 | returnWithError error
23 | panicNow bool
24 | }
25 |
26 | type taskInputMock struct {
27 | commandCh chan taskCommand
28 | runningTasks containers.ConcurrentMap[task.ID, bool]
29 | }
30 |
31 | type taskSpec task.ID
32 |
33 | func (spec taskSpec) ID() task.ID {
34 | return task.ID(spec)
35 | }
36 |
37 | func (spec taskSpec) RunTask(ctx context.Context, _ *zap.Logger, in *taskInputMock) error {
38 | in.runningTasks.Set(task.ID(spec), true)
39 | defer in.runningTasks.Set(task.ID(spec), false)
40 |
41 | select {
42 | case <-ctx.Done():
43 | return nil
44 | case cmd := <-in.commandCh:
45 | if cmd.panicNow {
46 | panic("panic")
47 | }
48 |
49 | return cmd.returnWithError
50 | }
51 | }
52 |
53 | func TestTask(t *testing.T) {
54 | t.Parallel()
55 |
56 | logger := zaptest.NewLogger(t)
57 | ctx := t.Context()
58 |
59 | assert := assert.New(t)
60 |
61 | in := &taskInputMock{
62 | commandCh: make(chan taskCommand),
63 | }
64 |
65 | assertTask := func(id string, expectedRunning bool) {
66 | assert.Eventually(func() bool {
67 | running, _ := in.runningTasks.Get(id)
68 |
69 | return running == expectedRunning
70 | }, 3*time.Second, time.Millisecond)
71 | }
72 |
73 | t1 := task.New(logger, taskSpec("task1"), in)
74 | t1.Start(ctx)
75 |
76 | assertTask("task1", true)
77 |
78 | // should restart on panic
79 | in.commandCh <- taskCommand{
80 | panicNow: true,
81 | }
82 |
83 | assertTask("task1", false)
84 | assertTask("task1", true)
85 |
86 | // short restart on error
87 | in.commandCh <- taskCommand{
88 | returnWithError: errors.New("failed"),
89 | }
90 |
91 | assertTask("task1", false)
92 | assertTask("task1", true)
93 |
94 | t1.Stop()
95 | assertTask("task1", false)
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/rruntime/state.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package rruntime
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/cosi-project/runtime/pkg/controller"
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | "github.com/cosi-project/runtime/pkg/state/owned"
13 | )
14 |
15 | // Create augments StateAdapter Create with output tracking.
16 | func (adapter *Adapter) Create(ctx context.Context, r resource.Resource, options ...owned.CreateOption) error {
17 | err := adapter.StateAdapter.Create(ctx, r, options...)
18 |
19 | if adapter.outputTracker != nil {
20 | adapter.outputTracker[makeOutputTrackingID(r.Metadata())] = struct{}{}
21 | }
22 |
23 | return err
24 | }
25 |
26 | // Update augments StateAdapter Update with output tracking.
27 | func (adapter *Adapter) Update(ctx context.Context, newResource resource.Resource) error {
28 | err := adapter.StateAdapter.Update(ctx, newResource)
29 |
30 | if adapter.outputTracker != nil {
31 | adapter.outputTracker[makeOutputTrackingID(newResource.Metadata())] = struct{}{}
32 | }
33 |
34 | return err
35 | }
36 |
37 | // Modify augments StateAdapter Modify with output tracking.
38 | func (adapter *Adapter) Modify(ctx context.Context, emptyResource resource.Resource, updateFunc func(resource.Resource) error, options ...controller.ModifyOption) error {
39 | err := adapter.StateAdapter.Modify(ctx, emptyResource, updateFunc, options...)
40 |
41 | if adapter.outputTracker != nil {
42 | adapter.outputTracker[makeOutputTrackingID(emptyResource.Metadata())] = struct{}{}
43 | }
44 |
45 | return err
46 | }
47 |
48 | // ModifyWithResult augments StateAdapter ModifyWithResult with output tracking.
49 | func (adapter *Adapter) ModifyWithResult(
50 | ctx context.Context, emptyResource resource.Resource, updateFunc func(resource.Resource) error, options ...controller.ModifyOption,
51 | ) (resource.Resource, error) {
52 | result, err := adapter.StateAdapter.ModifyWithResult(ctx, emptyResource, updateFunc, options...)
53 |
54 | if adapter.outputTracker != nil {
55 | adapter.outputTracker[makeOutputTrackingID(emptyResource.Metadata())] = struct{}{}
56 | }
57 |
58 | return result, err
59 | }
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # runtime
2 |
3 | COSI Runtime contains core resource (state) and controller (operator) engine to build operating systems.
4 |
5 | ## Design
6 |
7 | ### Resources
8 |
9 | A **resource** is a _metadata_ plus opaque _spec_.
10 | Metadata structure is strictly defined, while a spec is transparent to the `runtime`.
11 | Metadata defines an address of the resource: (namespace, type, id, version) and additional fields (finalizers, owner, etc.)
12 |
13 | ### Controllers
14 |
15 | A **controller** is a task that runs as a single thread of execution.
16 | A controller has defined _input_ and _outputs_.
17 | Outputs are static and should be defined at the registration time, while inputs are dynamic and might change during controller execution.
18 |
19 | A controller is supposed to implement a reconcile loop: for each reconcile event (coming from the runtime) the controller wakes up, checks the inputs,
20 | performs any actions and modifies the outputs.
21 |
22 | Controller inputs are resources which controller can read (it can't read resources that are not declared as inputs), and inputs are the resources controller
23 | gets notified about changes:
24 |
25 | * `strong` inputs are the inputs controller depends on in a strong way: it has to be notified when inputs are going to be destroyed via finalizer mechanism;
26 | * `weak` inputs are the inputs controller watches, but it doesn't have to do any cleanup when weak inputs are being destroyed.
27 |
28 | A controller can modify finalizers of strong controller inputs; any other modifications to the inputs are not permitted.
29 |
30 | Controller outputs are resources which controller can write (create, destroy, update):
31 |
32 | * `exclusive` outputs are managed by only a single controller; no other controller can modify exclusive resources
33 | * `shared` outputs are resources that are created by multiple controllers, but each specific resource can only be modified by a controller which created that resource
34 |
35 | Runtime verifies that only one controller has `exclusive` access to the resource.
36 |
37 | ### Principles
38 |
39 | * simple and structured: impose structure to make things simple.
40 | * avoid conflicts by design: resources don't have multiple entities which can modify them.
41 | * use controller structure as documentation: graph of dependencies between controllers and resources documents system design and current state.
42 |
--------------------------------------------------------------------------------
/pkg/resource/protobuf/yaml.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package protobuf
6 |
7 | import (
8 | "fmt"
9 |
10 | "go.yaml.in/yaml/v4"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | )
14 |
15 | // YAMLResource is a wrapper around Resource which implements yaml.Unmarshaler.
16 | // Its here and not in resource package to avoid circular dependency.
17 | type YAMLResource struct {
18 | r resource.Resource
19 | }
20 |
21 | // Resource returns the underlying resource.
22 | func (r *YAMLResource) Resource() resource.Resource {
23 | if r.r == nil {
24 | panic("resource is not set")
25 | }
26 |
27 | return r.r.DeepCopy()
28 | }
29 |
30 | // UnmarshalYAML implements yaml.Unmarshaler.
31 | func (r *YAMLResource) UnmarshalYAML(value *yaml.Node) error {
32 | if value.Kind != yaml.MappingNode {
33 | return fmt.Errorf("expected mapping node, got %d", value.Kind)
34 | }
35 |
36 | if len(value.Content) != 4 {
37 | return fmt.Errorf("expected 4 elements node, got %d", len(value.Content))
38 | }
39 |
40 | var mdNode, specNode *yaml.Node
41 |
42 | for i := 0; i < len(value.Content); i += 2 {
43 | key := value.Content[i]
44 | val := value.Content[i+1]
45 |
46 | if key.Kind != yaml.ScalarNode {
47 | return fmt.Errorf("expected scalar node, got %d", key.Kind)
48 | }
49 |
50 | if val.Kind != yaml.MappingNode {
51 | return fmt.Errorf("expected mapping node, got %d", value.Content[i+1].Kind)
52 | }
53 |
54 | switch key.Value {
55 | case "metadata":
56 | mdNode = val
57 | case "spec":
58 | specNode = val
59 | default:
60 | return fmt.Errorf("unexpected key %v", key)
61 | }
62 | }
63 |
64 | if mdNode == nil || specNode == nil {
65 | return fmt.Errorf("metadata or spec node is missing")
66 | }
67 |
68 | var md resource.Metadata
69 |
70 | err := md.UnmarshalYAML(mdNode)
71 | if err != nil {
72 | return fmt.Errorf("failed to unmarshal metadata: %w", err)
73 | }
74 |
75 | result, err := CreateResource(md.Type())
76 | if err != nil {
77 | return fmt.Errorf("failed to create resource: %w", err)
78 | }
79 |
80 | *result.Metadata() = md
81 |
82 | err = specNode.Decode(result.Spec())
83 | if err != nil {
84 | return fmt.Errorf("failed to unmarshal spec: %w", err)
85 | }
86 |
87 | r.r = result
88 |
89 | return nil
90 | }
91 |
--------------------------------------------------------------------------------
/pkg/state/protobuf/client/label_query.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package client
6 |
7 | import (
8 | "fmt"
9 |
10 | "github.com/cosi-project/runtime/api/v1alpha1"
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | )
13 |
14 | func transformLabelQuery(input resource.LabelQuery) (*v1alpha1.LabelQuery, error) {
15 | labelQuery := &v1alpha1.LabelQuery{
16 | Terms: make([]*v1alpha1.LabelTerm, 0, len(input.Terms)),
17 | }
18 |
19 | for _, term := range input.Terms {
20 | switch term.Op {
21 | case resource.LabelOpEqual:
22 | labelQuery.Terms = append(labelQuery.Terms, &v1alpha1.LabelTerm{
23 | Key: term.Key,
24 | Value: term.Value,
25 | Op: v1alpha1.LabelTerm_EQUAL,
26 | Invert: term.Invert,
27 | })
28 | case resource.LabelOpExists:
29 | labelQuery.Terms = append(labelQuery.Terms, &v1alpha1.LabelTerm{
30 | Key: term.Key,
31 | Op: v1alpha1.LabelTerm_EXISTS,
32 | Invert: term.Invert,
33 | })
34 | case resource.LabelOpIn:
35 | labelQuery.Terms = append(labelQuery.Terms, &v1alpha1.LabelTerm{
36 | Key: term.Key,
37 | Value: term.Value,
38 | Op: v1alpha1.LabelTerm_IN,
39 | Invert: term.Invert,
40 | })
41 | case resource.LabelOpLT:
42 | labelQuery.Terms = append(labelQuery.Terms, &v1alpha1.LabelTerm{
43 | Key: term.Key,
44 | Value: term.Value,
45 | Op: v1alpha1.LabelTerm_LT,
46 | Invert: term.Invert,
47 | })
48 | case resource.LabelOpLTE:
49 | labelQuery.Terms = append(labelQuery.Terms, &v1alpha1.LabelTerm{
50 | Key: term.Key,
51 | Value: term.Value,
52 | Op: v1alpha1.LabelTerm_LTE,
53 | Invert: term.Invert,
54 | })
55 | case resource.LabelOpLTNumeric:
56 | labelQuery.Terms = append(labelQuery.Terms, &v1alpha1.LabelTerm{
57 | Key: term.Key,
58 | Value: term.Value,
59 | Op: v1alpha1.LabelTerm_LT_NUMERIC,
60 | Invert: term.Invert,
61 | })
62 | case resource.LabelOpLTENumeric:
63 | labelQuery.Terms = append(labelQuery.Terms, &v1alpha1.LabelTerm{
64 | Key: term.Key,
65 | Value: term.Value,
66 | Op: v1alpha1.LabelTerm_LTE_NUMERIC,
67 | Invert: term.Invert,
68 | })
69 | default:
70 | return nil, fmt.Errorf("unsupported label query operator: %v", term.Op)
71 | }
72 | }
73 |
74 | return labelQuery, nil
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/resource/labels.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource
6 |
7 | import (
8 | "fmt"
9 | "slices"
10 |
11 | "github.com/siderolabs/go-pointer"
12 |
13 | "github.com/cosi-project/runtime/pkg/resource/internal/compare"
14 | "github.com/cosi-project/runtime/pkg/resource/internal/kv"
15 | )
16 |
17 | // Labels is a set free-form of key-value pairs.
18 | //
19 | // Order of keys is not guaranteed.
20 | //
21 | // Labels support copy-on-write semantics, so metadata copies share common labels as long as possible.
22 | // Labels support querying with LabelTerm.
23 | type Labels struct {
24 | kv.KV
25 | }
26 |
27 | // Equal checks labels for equality.
28 | func (labels Labels) Equal(other Labels) bool {
29 | return labels.KV.Equal(other.KV)
30 | }
31 |
32 | // Matches if labels match the LabelTerm.
33 | func (labels Labels) Matches(term LabelTerm) bool {
34 | matches := labels.matches(term)
35 |
36 | if matches == nil {
37 | return false
38 | }
39 |
40 | m := *matches
41 |
42 | if term.Invert {
43 | return !m
44 | }
45 |
46 | return m
47 | }
48 |
49 | func (labels Labels) matches(term LabelTerm) *bool {
50 | if labels.Empty() && term.Op == LabelOpExists {
51 | return pointer.To(false)
52 | }
53 |
54 | value, ok := labels.Get(term.Key)
55 |
56 | if !ok {
57 | if term.Op.isComparison() {
58 | return nil
59 | }
60 |
61 | return pointer.To(false)
62 | }
63 |
64 | if term.Op != LabelOpExists && len(term.Value) == 0 {
65 | return pointer.To(false)
66 | }
67 |
68 | switch term.Op {
69 | case LabelOpExists:
70 | return pointer.To(true)
71 | case LabelOpEqual:
72 | return pointer.To(value == term.Value[0])
73 | case LabelOpIn:
74 | return pointer.To(slices.Contains(term.Value, value))
75 | case LabelOpLTE:
76 | return pointer.To(value <= term.Value[0])
77 | case LabelOpLT:
78 | return pointer.To(value < term.Value[0])
79 | case LabelOpLTNumeric:
80 | left, right, ok := compare.GetNumbers(value, term.Value[0])
81 | if !ok {
82 | return nil
83 | }
84 |
85 | return pointer.To(left < right)
86 | case LabelOpLTENumeric:
87 | left, right, ok := compare.GetNumbers(value, term.Value[0])
88 | if !ok {
89 | return nil
90 | }
91 |
92 | return pointer.To(left <= right)
93 | default:
94 | panic(fmt.Sprintf("unsupported label term operator: %v", term.Op))
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/resource/internal/compare/compare_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package compare_test
6 |
7 | import (
8 | "fmt"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/cosi-project/runtime/pkg/resource/internal/compare"
14 | )
15 |
16 | func TestCompare(t *testing.T) {
17 | for _, tt := range []struct {
18 | check func(assertions *require.Assertions, left int64, right int64, ok bool)
19 | left string
20 | right string
21 | }{
22 | {
23 | left: "4GiB",
24 | right: "4GB",
25 | check: func(assertions *require.Assertions, left, right int64, ok bool) {
26 | assertions.True(ok)
27 | assertions.Less(right, left)
28 | assertions.Equal(int64(4e9), right)
29 | assertions.Equal(int64(4*1<<30), left)
30 | },
31 | },
32 | {
33 | left: "1",
34 | right: "2000",
35 | check: func(assertions *require.Assertions, left, right int64, ok bool) {
36 | assertions.True(ok)
37 | assertions.Less(left, right)
38 | assertions.Equal(int64(1), left)
39 | assertions.Equal(int64(2000), right)
40 | },
41 | },
42 | {
43 | left: "1 1",
44 | right: "2000 3",
45 | check: func(assertions *require.Assertions, _, _ int64, ok bool) {
46 | assertions.False(ok)
47 | },
48 | },
49 | {
50 | left: " 1 k",
51 | right: "2000",
52 | check: func(assertions *require.Assertions, left, right int64, ok bool) {
53 | assertions.True(ok)
54 | assertions.Less(left, right)
55 | assertions.Equal(int64(1000), left)
56 | assertions.Equal(int64(2000), right)
57 | },
58 | },
59 | {
60 | left: "-1 k",
61 | right: "2000",
62 | check: func(assertions *require.Assertions, left, _ int64, ok bool) {
63 | assertions.True(ok)
64 | assertions.Equal(int64(-1000), left)
65 | },
66 | },
67 | {
68 | left: "1.1 k",
69 | check: func(assertions *require.Assertions, _, _ int64, ok bool) {
70 | assertions.False(ok)
71 | },
72 | },
73 | {
74 | left: "1 i",
75 | check: func(assertions *require.Assertions, _, _ int64, ok bool) {
76 | assertions.False(ok)
77 | },
78 | },
79 | } {
80 | t.Run(fmt.Sprintf("left %s, right %s", tt.left, tt.right), func(t *testing.T) {
81 | left, right, ok := compare.GetNumbers(tt.left, tt.right)
82 |
83 | tt.check(require.New(t), left, right, ok)
84 | })
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/qitem.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package qruntime
6 |
7 | import (
8 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/reduced"
9 | "github.com/cosi-project/runtime/pkg/resource"
10 | )
11 |
12 | // QJob is a job to be executed by the reconcile queue.
13 | type QJob int
14 |
15 | // QJob constants.
16 | const (
17 | QJobReconcile QJob = iota
18 | QJobMap
19 | )
20 |
21 | func (job QJob) String() string {
22 | switch job {
23 | case QJobReconcile:
24 | return "reconcile"
25 | case QJobMap:
26 | return "map"
27 | default:
28 | return "unknown"
29 | }
30 | }
31 |
32 | // NewQItem creates a new QItem.
33 | func NewQItem(md *resource.Metadata, job QJob) QItem {
34 | red := reduced.NewMetadata(md)
35 |
36 | return NewQItemFromReduced(&red, job)
37 | }
38 |
39 | // NewQItemFromReduced creates a new QItem from a reduced Metadata.
40 | func NewQItemFromReduced(md *reduced.Metadata, job QJob) QItem {
41 | return QItem{
42 | QKey: QKey{
43 | key: md.Key,
44 | job: job,
45 | },
46 | QValue: QValue{
47 | value: md.Value,
48 | },
49 | }
50 | }
51 |
52 | // QKey is the key of the reconcile queue.
53 | type QKey struct {
54 | key reduced.Key
55 |
56 | job QJob
57 | }
58 |
59 | // QValue is the value of the reconcile queue.
60 | type QValue struct {
61 | value reduced.Value
62 | }
63 |
64 | // QItem is a key-value pair stored in the reconcole queue.
65 | type QItem struct {
66 | QKey
67 | QValue
68 | }
69 |
70 | // Namespace implements resource.Pointer interface.
71 | func (item QKey) Namespace() resource.Namespace {
72 | return item.key.Namespace
73 | }
74 |
75 | // Type implements resource.Pointer interface.
76 | func (item QKey) Type() resource.Type {
77 | return item.key.Typ
78 | }
79 |
80 | // ID implements resource.Pointer interface.
81 | func (item QKey) ID() resource.ID {
82 | return item.key.ID
83 | }
84 |
85 | // Phase implements ReducedResourceMetadata interface.
86 | func (item QValue) Phase() resource.Phase {
87 | return item.value.Phase
88 | }
89 |
90 | // FinalizersEmpty implements ReducedResourceMetadata interface.
91 | func (item QValue) FinalizersEmpty() bool {
92 | return item.value.FinalizersEmpty
93 | }
94 |
95 | // Labels implements ReducedResourceMetadata interface.
96 | func (item QValue) Labels() *resource.Labels {
97 | return item.value.Labels
98 | }
99 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/compression/compression.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package compression provides compression support for [store.Marshaler].
6 | package compression
7 |
8 | import (
9 | "fmt"
10 |
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | "github.com/cosi-project/runtime/pkg/state/impl/store"
13 | )
14 |
15 | // Marshaler compresses and decompresses data from the underlying marshaler.
16 | //
17 | // Marshaler also handles case when the underlying data is not compressed.
18 | //
19 | // The trick used is that `0x00` can't start a valid protobuf message, so we use
20 | // `0x00` as a marker for compressed data.
21 | type Marshaler struct {
22 | underlying store.Marshaler
23 | compressor Compressor
24 | minSize int
25 | }
26 |
27 | // Compressor defines interface for compression and decompression.
28 | type Compressor interface {
29 | Compress(prefix, data []byte) ([]byte, error)
30 | Decompress(data []byte) ([]byte, error)
31 | ID() byte
32 | }
33 |
34 | // NewMarshaler creates new Marshaler.
35 | func NewMarshaler(m store.Marshaler, c Compressor, minSize int) *Marshaler {
36 | return &Marshaler{underlying: m, compressor: c, minSize: minSize}
37 | }
38 |
39 | // MarshalResource implements Marshaler interface.
40 | func (m *Marshaler) MarshalResource(r resource.Resource) ([]byte, error) {
41 | encoded, err := m.underlying.MarshalResource(r)
42 | if err != nil {
43 | return nil, fmt.Errorf("failed to marshal resource: %w", err)
44 | }
45 |
46 | if len(encoded) < m.minSize {
47 | return encoded, nil
48 | }
49 |
50 | compressed, err := m.compressor.Compress([]byte{0x0, m.compressor.ID()}, encoded)
51 | if err != nil {
52 | return nil, fmt.Errorf("failed to compress: %w", err)
53 | }
54 |
55 | return compressed, nil
56 | }
57 |
58 | // UnmarshalResource implements Marshaler interface.
59 | func (m *Marshaler) UnmarshalResource(b []byte) (resource.Resource, error) { //nolint:ireturn
60 | if len(b) > 1 && b[0] == 0x0 {
61 | id := b[1]
62 |
63 | if id != m.compressor.ID() {
64 | return nil, fmt.Errorf("unknown compression ID: %d", id)
65 | }
66 |
67 | var err error
68 |
69 | // Data is compressed, decompress it.
70 | b, err = m.compressor.Decompress(b[2:])
71 | if err != nil {
72 | return nil, fmt.Errorf("failed to decompress: %w", err)
73 | }
74 | }
75 |
76 | return m.underlying.UnmarshalResource(b)
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/resource/protobuf/resource_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package protobuf_test
6 |
7 | import (
8 | "testing"
9 | "time"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | "go.yaml.in/yaml/v4"
14 | "google.golang.org/protobuf/types/known/timestamppb"
15 |
16 | "github.com/cosi-project/runtime/api/v1alpha1"
17 | "github.com/cosi-project/runtime/pkg/resource"
18 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
19 | )
20 |
21 | func TestMarshalUnmarshal(t *testing.T) {
22 | t.Parallel()
23 |
24 | created, _ := time.Parse(time.RFC3339, "2021-06-23T19:22:29Z") //nolint:errcheck
25 | updated, _ := time.Parse(time.RFC3339, "2021-06-23T20:22:29Z") //nolint:errcheck
26 |
27 | protoR := &v1alpha1.Resource{
28 | Metadata: &v1alpha1.Metadata{
29 | Namespace: "ns",
30 | Type: "typ",
31 | Id: "id",
32 | Version: "3",
33 | Owner: "FooController",
34 | Phase: "running",
35 | Created: timestamppb.New(created),
36 | Updated: timestamppb.New(updated),
37 | Finalizers: []string{"a1", "a2"},
38 | Annotations: map[string]string{
39 | "ttl": "1h",
40 | },
41 | Labels: map[string]string{
42 | "app": "foo",
43 | "stage": "initial",
44 | },
45 | },
46 | Spec: &v1alpha1.Spec{
47 | YamlSpec: "true",
48 | ProtoSpec: []byte("test"),
49 | },
50 | }
51 |
52 | r, err := protobuf.Unmarshal(protoR)
53 | require.NoError(t, err)
54 |
55 | protoR2, err := r.Marshal()
56 | require.NoError(t, err)
57 |
58 | assert.True(t, protobuf.ProtoEqual(protoR, protoR2))
59 |
60 | r2, err := protobuf.Unmarshal(protoR2)
61 | require.NoError(t, err)
62 |
63 | assert.True(t, resource.Equal(r, r2))
64 |
65 | assert.True(t, resource.Equal(r, r.DeepCopy()))
66 |
67 | y, err := resource.MarshalYAML(r)
68 | require.NoError(t, err)
69 |
70 | yy, err := yaml.Marshal(y)
71 | require.NoError(t, err)
72 |
73 | assert.Equal(t, `metadata:
74 | namespace: ns
75 | type: typ
76 | id: id
77 | version: 3
78 | owner: FooController
79 | phase: running
80 | created: 2021-06-23T19:22:29Z
81 | updated: 2021-06-23T20:22:29Z
82 | labels:
83 | app: foo
84 | stage: initial
85 | annotations:
86 | ttl: 1h
87 | finalizers:
88 | - a1
89 | - a2
90 | spec: true
91 | `,
92 | string(yy))
93 | }
94 |
--------------------------------------------------------------------------------
/pkg/safe/util.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package safe
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/cosi-project/runtime/pkg/controller"
11 | "github.com/cosi-project/runtime/pkg/controller/generic"
12 | "github.com/cosi-project/runtime/pkg/controller/runtime/options"
13 | "github.com/cosi-project/runtime/pkg/resource"
14 | )
15 |
16 | // Map applies the given function to each element of the list and returns a new slice with the results. It
17 | // returns an error if the given function had returned an error.
18 | func Map[T resource.Resource, R any](list List[T], fn func(T) (R, error)) ([]R, error) {
19 | result := make([]R, 0, list.Len())
20 |
21 | for _, item := range list.list.Items {
22 | r, err := fn(item.(T)) //nolint:errcheck,forcetypeassert
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | result = append(result, r)
28 | }
29 |
30 | return result, nil
31 | }
32 |
33 | // ToSlice applies the given function to each element of the list and returns a new slice with the results.
34 | func ToSlice[T resource.Resource, R any](list List[T], fn func(T) R) []R {
35 | result := make([]R, 0, list.Len())
36 |
37 | for _, item := range list.list.Items {
38 | result = append(result, fn(item.(T))) //nolint:forcetypeassert,errcheck
39 | }
40 |
41 | return result
42 | }
43 |
44 | // Input returns a controller.Input for the given resource.
45 | func Input[R generic.ResourceWithRD](kind controller.InputKind) controller.Input {
46 | var r R
47 |
48 | return controller.Input{
49 | Namespace: r.ResourceDefinition().DefaultNamespace,
50 | Type: r.ResourceDefinition().Type,
51 | Kind: kind,
52 | }
53 | }
54 |
55 | // CleanupOutputs wraps the controller.OutputTracker.CleanupOutputs method.
56 | func CleanupOutputs[R generic.ResourceWithRD](ctx context.Context, tracker controller.OutputTracker) error {
57 | var r R
58 |
59 | return tracker.CleanupOutputs(ctx,
60 | resource.NewMetadata(
61 | r.ResourceDefinition().DefaultNamespace,
62 | r.ResourceDefinition().Type,
63 | "",
64 | resource.VersionUndefined,
65 | ),
66 | )
67 | }
68 |
69 | // WithResourceCache returns a controller runtime options.WithResourceCache.
70 | func WithResourceCache[R generic.ResourceWithRD]() options.Option {
71 | var r R
72 |
73 | return options.WithCachedResource(r.ResourceDefinition().DefaultNamespace, r.ResourceDefinition().Type)
74 | }
75 |
--------------------------------------------------------------------------------
/pkg/resource/protobuf/yaml_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package protobuf_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/siderolabs/gen/ensure"
11 | "github.com/siderolabs/go-pointer"
12 | "github.com/stretchr/testify/require"
13 | "go.yaml.in/yaml/v4"
14 |
15 | "github.com/cosi-project/runtime/api/v1alpha1"
16 | "github.com/cosi-project/runtime/pkg/resource"
17 | "github.com/cosi-project/runtime/pkg/resource/meta"
18 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
19 | "github.com/cosi-project/runtime/pkg/resource/typed"
20 | )
21 |
22 | func init() {
23 | ensure.NoError(protobuf.RegisterResource(TestType, &TestResource{}))
24 | }
25 |
26 | // TestNamespaceName is the namespace of Test resource.
27 | const TestNamespaceName = resource.Namespace("ns-event")
28 |
29 | // TestType is the type of Test.
30 | const TestType = resource.Type("Test.test.cosi.dev")
31 |
32 | type (
33 | // TestResource is a test resource.
34 | TestResource = typed.Resource[TestSpec, TestExtension]
35 | )
36 |
37 | // NewTestResource initializes TestResource resource.
38 | func NewTestResource(id resource.ID, spec TestSpec) *TestResource {
39 | return typed.NewResource[TestSpec, TestExtension](
40 | resource.NewMetadata(TestNamespaceName, TestType, id, resource.VersionUndefined),
41 | spec,
42 | )
43 | }
44 |
45 | // TestExtension provides auxiliary methods for A.
46 | type TestExtension struct{}
47 |
48 | // ResourceDefinition implements core.ResourceDefinitionProvider interface.
49 | func (TestExtension) ResourceDefinition() meta.ResourceDefinitionSpec {
50 | return meta.ResourceDefinitionSpec{
51 | Type: TestType,
52 | DefaultNamespace: TestNamespaceName,
53 | }
54 | }
55 |
56 | type TestSpec = protobuf.ResourceSpec[v1alpha1.UpdateOptions, *v1alpha1.UpdateOptions]
57 |
58 | func TestYAMLResource(t *testing.T) {
59 | original := NewTestResource("id", TestSpec{
60 | Value: &v1alpha1.UpdateOptions{
61 | Owner: "some owner",
62 | ExpectedPhase: pointer.To(resource.Phase(0).String()),
63 | },
64 | })
65 |
66 | strct, err := resource.MarshalYAML(original)
67 | require.NoError(t, err)
68 |
69 | raw, err := yaml.Marshal(strct)
70 | require.NoError(t, err)
71 |
72 | var result protobuf.YAMLResource
73 |
74 | err = yaml.Unmarshal(raw, &result)
75 | require.NoError(t, err)
76 | require.True(t, resource.Equal(original, result.Resource()))
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/metrics/metrics.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package metrics expose various controller runtime metrics using expvar.
6 | package metrics
7 |
8 | import (
9 | "expvar"
10 | )
11 |
12 | var (
13 | // ControllerCrashes counts the number of crashes per Controller.
14 | ControllerCrashes = expvar.NewMap("controller_crashes")
15 |
16 | // ControllerWakeups counts the number of wakeups per Controller.
17 | ControllerWakeups = expvar.NewMap("controller_wakeups")
18 |
19 | // ControllerReads counts the number of reads per controller (both Controller and QController).
20 | //
21 | // Each call to controller.Reader is counted as a single read.
22 | ControllerReads = expvar.NewMap("controller_reads")
23 |
24 | // ControllerWrites counts the number of writes per controller (both Controller and QController).
25 | //
26 | // Each call to controller.Writer is counted as a single write.
27 | ControllerWrites = expvar.NewMap("controller_writes")
28 |
29 | // QControllerCrashes counts the number of crashes per QController.
30 | QControllerCrashes = expvar.NewMap("qcontroller_crashes")
31 |
32 | // QControllerRequeues counts the number of requeue events per QController.
33 | QControllerRequeues = expvar.NewMap("qcontroller_requeues")
34 |
35 | // QControllerProcessed counts the number of processed reconcile events per QController.
36 | QControllerProcessed = expvar.NewMap("qcontroller_processed")
37 |
38 | // QControllerMappedIn counts the number of map events per QController.
39 | QControllerMappedIn = expvar.NewMap("qcontroller_mapped_in")
40 |
41 | // QControllerMappedOut counts the number outputs for map events per QController.
42 | QControllerMappedOut = expvar.NewMap("qcontroller_mapped_out")
43 |
44 | // QControllerQueueLength reports the outstanding queue length per QController (both map and reconcile events).
45 | QControllerQueueLength = expvar.NewMap("qcontroller_queue_length")
46 |
47 | // QControllerMapBusy reports the number of seconds QController was busy processing map events.
48 | QControllerMapBusy = expvar.NewMap("qcontroller_map_busy")
49 |
50 | // QControllerReconcileBusy reports the number of seconds QController was busy processing reconcile events.
51 | QControllerReconcileBusy = expvar.NewMap("qcontroller_reconcile_busy")
52 |
53 | // CachedResources reports the number of cached resources per resource type.
54 | CachedResources = expvar.NewMap("cached_resources")
55 | )
56 |
--------------------------------------------------------------------------------
/pkg/resource/meta/namespace.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package meta
6 |
7 | import (
8 | "github.com/siderolabs/gen/ensure"
9 |
10 | "github.com/cosi-project/runtime/api/v1alpha1"
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
13 | "github.com/cosi-project/runtime/pkg/resource/typed"
14 | )
15 |
16 | // NamespaceType is the type of Namespace.
17 | const NamespaceType = resource.Type("Namespaces.meta.cosi.dev")
18 |
19 | // Namespace provides metadata about namespaces.
20 | type Namespace = typed.Resource[NamespaceSpec, NamespaceExtension]
21 |
22 | // NewNamespace initializes a Namespace resource.
23 | func NewNamespace(id resource.ID, spec NamespaceSpec) *Namespace {
24 | return typed.NewResource[NamespaceSpec, NamespaceExtension](
25 | resource.NewMetadata(NamespaceName, NamespaceType, id, resource.VersionUndefined),
26 | spec,
27 | )
28 | }
29 |
30 | // NamespaceExtension provides auxiliary methods for Namespace.
31 | type NamespaceExtension struct{}
32 |
33 | // ResourceDefinition implements core.ResourceDefinitionProvider interface.
34 | func (NamespaceExtension) ResourceDefinition() ResourceDefinitionSpec {
35 | return ResourceDefinitionSpec{
36 | Type: NamespaceType,
37 | DefaultNamespace: NamespaceName,
38 | Aliases: []resource.Type{"ns"},
39 | }
40 | }
41 |
42 | // NamespaceSpec provides Namespace definition.
43 | //
44 | //nolint:recvcheck
45 | type NamespaceSpec struct {
46 | Description string `yaml:"description"`
47 | }
48 |
49 | // DeepCopy generates a deep copy of NamespaceSpec.
50 | func (n NamespaceSpec) DeepCopy() NamespaceSpec {
51 | return n
52 | }
53 |
54 | // MarshalProto implements ProtoMarshaler.
55 | func (n NamespaceSpec) MarshalProto() ([]byte, error) {
56 | protoSpec := v1alpha1.NamespaceSpec{
57 | Description: n.Description,
58 | }
59 |
60 | return protobuf.ProtoMarshal(&protoSpec)
61 | }
62 |
63 | // UnmarshalProto implements protobuf.ResourceUnmarshaler.
64 | func (n *NamespaceSpec) UnmarshalProto(protoBytes []byte) error {
65 | protoSpec := v1alpha1.NamespaceSpec{}
66 |
67 | if err := protobuf.ProtoUnmarshal(protoBytes, &protoSpec); err != nil {
68 | return err
69 | }
70 |
71 | *n = NamespaceSpec{
72 | Description: protoSpec.Description,
73 | }
74 |
75 | return nil
76 | }
77 |
78 | func init() {
79 | ensure.NoError(protobuf.RegisterResource(NamespaceType, &Namespace{}))
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/state/filter_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package state_test
6 |
7 | import (
8 | "context"
9 | "fmt"
10 | "testing"
11 |
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/require"
14 | "github.com/stretchr/testify/suite"
15 |
16 | "github.com/cosi-project/runtime/pkg/resource"
17 | "github.com/cosi-project/runtime/pkg/state"
18 | "github.com/cosi-project/runtime/pkg/state/conformance"
19 | "github.com/cosi-project/runtime/pkg/state/impl/inmem"
20 | "github.com/cosi-project/runtime/pkg/state/impl/namespaced"
21 | )
22 |
23 | func TestFilterPasshtroughConformance(t *testing.T) {
24 | t.Parallel()
25 |
26 | suite.Run(t, &conformance.StateSuite{
27 | State: state.WrapCore(
28 | state.Filter(
29 | namespaced.NewState(inmem.Build),
30 | func(context.Context, state.Access) error {
31 | return nil
32 | },
33 | ),
34 | ),
35 | Namespaces: []resource.Namespace{"default", "controller", "system", "runtime"},
36 | })
37 | }
38 |
39 | func TestFilterSingleResource(t *testing.T) {
40 | t.Parallel()
41 |
42 | const (
43 | namespace = "default"
44 | resourceType = conformance.PathResourceType
45 | resourceID = "/var/lib"
46 | )
47 |
48 | resources := state.WrapCore(
49 | state.Filter(
50 | namespaced.NewState(inmem.Build),
51 | func(_ context.Context, access state.Access) error {
52 | if access.ResourceNamespace != namespace || access.ResourceType != resourceType || access.ResourceID != resourceID {
53 | return fmt.Errorf("access denied")
54 | }
55 |
56 | if access.Verb == state.Watch {
57 | return fmt.Errorf("access denied")
58 | }
59 |
60 | return nil
61 | },
62 | ),
63 | )
64 |
65 | path := conformance.NewPathResource(namespace, resourceID)
66 | require.NoError(t, resources.Create(t.Context(), path))
67 |
68 | path2 := conformance.NewPathResource(namespace, resourceID+"/exta")
69 | require.Error(t, resources.Create(t.Context(), path2))
70 |
71 | _, err := resources.List(t.Context(), path.Metadata())
72 | require.Error(t, err)
73 |
74 | require.Error(t, resources.Watch(t.Context(), path.Metadata(), nil))
75 | require.Error(t, resources.WatchKind(t.Context(), path.Metadata(), nil))
76 |
77 | destroyReady, err := resources.Teardown(t.Context(), path.Metadata())
78 | require.NoError(t, err)
79 | assert.True(t, destroyReady)
80 |
81 | require.NoError(t, resources.Destroy(t.Context(), path.Metadata()))
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/state/protobuf/server/helpers.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package server
6 |
7 | import (
8 | "regexp"
9 |
10 | "google.golang.org/grpc/codes"
11 | "google.golang.org/grpc/status"
12 |
13 | "github.com/cosi-project/runtime/api/v1alpha1"
14 | "github.com/cosi-project/runtime/pkg/resource"
15 | )
16 |
17 | // ConvertLabelQuery converts protobuf representation of LabelQuery to state representation.
18 | func ConvertLabelQuery(terms []*v1alpha1.LabelTerm) ([]resource.LabelQueryOption, error) {
19 | labelOpts := make([]resource.LabelQueryOption, 0, len(terms))
20 |
21 | for _, term := range terms {
22 | var opts []resource.TermOption
23 |
24 | if term.Invert {
25 | opts = append(opts, resource.NotMatches)
26 | }
27 |
28 | switch term.Op {
29 | case v1alpha1.LabelTerm_EQUAL:
30 | labelOpts = append(labelOpts, resource.LabelEqual(term.Key, term.Value[0], opts...))
31 | case v1alpha1.LabelTerm_EXISTS:
32 | labelOpts = append(labelOpts, resource.LabelExists(term.Key, opts...))
33 | case v1alpha1.LabelTerm_NOT_EXISTS: //nolint:staticcheck
34 | labelOpts = append(labelOpts, resource.LabelExists(term.Key, resource.NotMatches))
35 | case v1alpha1.LabelTerm_IN:
36 | labelOpts = append(labelOpts, resource.LabelIn(term.Key, term.Value, opts...))
37 | case v1alpha1.LabelTerm_LT:
38 | labelOpts = append(labelOpts, resource.LabelLT(term.Key, term.Value[0], opts...))
39 | case v1alpha1.LabelTerm_LTE:
40 | labelOpts = append(labelOpts, resource.LabelLTE(term.Key, term.Value[0], opts...))
41 | case v1alpha1.LabelTerm_LT_NUMERIC:
42 | labelOpts = append(labelOpts, resource.LabelLTNumeric(term.Key, term.Value[0], opts...))
43 | case v1alpha1.LabelTerm_LTE_NUMERIC:
44 | labelOpts = append(labelOpts, resource.LabelLTENumeric(term.Key, term.Value[0], opts...))
45 | default:
46 | return nil, status.Errorf(codes.Unimplemented, "unsupported label query operator: %v", term.Op)
47 | }
48 | }
49 |
50 | return labelOpts, nil
51 | }
52 |
53 | // ConvertIDQuery converts protobuf representation of IDQuery to state representation.
54 | func ConvertIDQuery(input *v1alpha1.IDQuery) ([]resource.IDQueryOption, error) {
55 | if input == nil || input.Regexp == "" {
56 | return nil, nil
57 | }
58 |
59 | re, err := regexp.Compile(input.Regexp)
60 | if err != nil {
61 | return nil, status.Errorf(codes.InvalidArgument, "failed to compile regexp: %v", err)
62 | }
63 |
64 | return []resource.IDQueryOption{resource.IDRegexpMatch(re)}, nil
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/dependency/bench_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package dependency_test
6 |
7 | import (
8 | "strconv"
9 | "testing"
10 |
11 | "github.com/siderolabs/gen/optional"
12 | "github.com/stretchr/testify/require"
13 |
14 | "github.com/cosi-project/runtime/pkg/controller"
15 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/dependency"
16 | "github.com/cosi-project/runtime/pkg/resource"
17 | )
18 |
19 | func BenchmarkGetDependentControllers(b *testing.B) {
20 | db, err := dependency.NewDatabase()
21 | require.NoError(b, err)
22 |
23 | require.NoError(b, db.AddControllerInput("ConfigController", controller.Input{
24 | Namespace: "user",
25 | Type: "Config",
26 | Kind: controller.InputWeak,
27 | }))
28 |
29 | require.NoError(b, db.AddControllerInput("ConfigController", controller.Input{
30 | Namespace: "user",
31 | Type: "Source",
32 | Kind: controller.InputWeak,
33 | }))
34 |
35 | require.NoError(b, db.AddControllerInput("GreatController", controller.Input{
36 | Namespace: "user",
37 | Type: "Config",
38 | Kind: controller.InputStrong,
39 | }))
40 |
41 | in := controller.Input{
42 | Namespace: "user",
43 | Type: "Config",
44 | ID: optional.Some[resource.ID]("aaaa"),
45 | }
46 |
47 | b.ResetTimer()
48 |
49 | for range b.N {
50 | _, err := db.GetDependentControllers(in)
51 | if err != nil {
52 | b.FailNow()
53 | }
54 | }
55 | }
56 |
57 | func BenchmarkBuildDatabase(b *testing.B) {
58 | db, err := dependency.NewDatabase()
59 | require.NoError(b, err)
60 |
61 | b.ResetTimer()
62 |
63 | for i := range b.N {
64 | iS := strconv.Itoa(i)
65 | ctrl := "ConfigController" + iS
66 | typ := "Resource" + iS
67 | greatCtrl := "GreatController" + iS[0:1]
68 |
69 | require.NoError(b, db.AddControllerInput(ctrl, controller.Input{
70 | Namespace: "user",
71 | Type: "Config",
72 | Kind: controller.InputWeak,
73 | }))
74 |
75 | require.NoError(b, db.AddControllerInput(ctrl, controller.Input{
76 | Namespace: "user",
77 | Type: typ,
78 | Kind: controller.InputWeak,
79 | }))
80 |
81 | require.NoError(b, db.AddControllerOutput(ctrl, controller.Output{
82 | Type: typ,
83 | Kind: controller.OutputExclusive,
84 | }))
85 |
86 | require.NoError(b, db.AddControllerInput(greatCtrl, controller.Input{
87 | Namespace: "user",
88 | Type: typ,
89 | Kind: controller.InputStrong,
90 | }))
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/state/impl/inmem/errors_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package inmem_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | "github.com/cosi-project/runtime/pkg/state"
14 | "github.com/cosi-project/runtime/pkg/state/impl/inmem"
15 | )
16 |
17 | func TestErrors(t *testing.T) {
18 | t.Parallel()
19 |
20 | assert.True(t, state.IsNotFoundError(inmem.ErrNotFound(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined))))
21 | assert.True(t, state.IsConflictError(inmem.ErrAlreadyExists(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined))))
22 | assert.True(t, state.IsConflictError(inmem.ErrVersionConflict(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined), resource.VersionUndefined, resource.VersionUndefined)))
23 | assert.True(t, state.IsConflictError(inmem.ErrPendingFinalizers(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined))))
24 | assert.True(t, state.IsConflictError(inmem.ErrOwnerConflict(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined), "owner")))
25 | assert.True(t, state.IsOwnerConflictError(inmem.ErrOwnerConflict(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined), "owner")))
26 | assert.True(t, state.IsPhaseConflictError(inmem.ErrPhaseConflict(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined), resource.PhaseTearingDown)))
27 |
28 | assert.True(t, state.IsConflictError(inmem.ErrAlreadyExists(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined)), state.WithResourceNamespace("ns")))
29 | assert.False(t, state.IsConflictError(inmem.ErrAlreadyExists(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined)), state.WithResourceNamespace("a")))
30 |
31 | assert.True(t, state.IsConflictError(inmem.ErrAlreadyExists(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined)), state.WithResourceType("a")))
32 | assert.False(t, state.IsConflictError(inmem.ErrAlreadyExists(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined)), state.WithResourceType("z")))
33 |
34 | assert.True(t, state.IsConflictError(inmem.ErrAlreadyExists(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined)), state.WithResourceType("a"), state.WithResourceNamespace("ns")))
35 | assert.False(t, state.IsConflictError(inmem.ErrAlreadyExists(resource.NewMetadata("ns", "a", "b", resource.VersionUndefined)), state.WithResourceType("z"), state.WithResourceNamespace("ns")))
36 |
37 | assert.True(t, state.IsInvalidWatchBookmarkError(inmem.ErrInvalidWatchBookmark))
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/options/options.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package options provides functional options for controller runtime.
6 | package options
7 |
8 | import (
9 | "golang.org/x/time/rate"
10 |
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | )
13 |
14 | // Options configures controller runtime.
15 | type Options struct {
16 | // CachedResources is a list of resources that should be cached by controller runtime.
17 | CachedResources []CachedResource
18 | // ChangeRateLimit and ChangeBurst configure rate limiting of changes performed by controllers.
19 | ChangeRateLimit rate.Limit
20 | ChangeBurst int
21 | // MetricsEnabled enables runtime metrics to be exposed via metrics package.
22 | MetricsEnabled bool
23 | // WarnOnUncachedReads adds a warning log when a controller reads an uncached resource.
24 | WarnOnUncachedReads bool
25 | }
26 |
27 | // CachedResource is a resource that should be cached by controller runtime.
28 | type CachedResource struct {
29 | Namespace resource.Namespace
30 | Type resource.Type
31 | }
32 |
33 | // Option is a functional option for controller runtime.
34 | type Option func(*Options)
35 |
36 | // WithChangeRateLimit sets rate limit for changes performed by controllers.
37 | //
38 | // This might be used to rate limit ill-behaving controllers from overloading the system with changes.
39 | func WithChangeRateLimit(limit rate.Limit, burst int) Option {
40 | return func(options *Options) {
41 | options.ChangeRateLimit = limit
42 | options.ChangeBurst = burst
43 | }
44 | }
45 |
46 | // WithMetrics enables runtime metrics to be exposed via metrics package.
47 | func WithMetrics(enabled bool) Option {
48 | return func(options *Options) {
49 | options.MetricsEnabled = enabled
50 | }
51 | }
52 |
53 | // WithCachedResource adds a resource to the list of resources that should be cached by controller runtime.
54 | func WithCachedResource(namespace resource.Namespace, typ resource.Type) Option {
55 | return func(options *Options) {
56 | options.CachedResources = append(options.CachedResources, CachedResource{
57 | Namespace: namespace,
58 | Type: typ,
59 | })
60 | }
61 | }
62 |
63 | // WithWarnOnUncachedReads adds a warning log when a controller reads an uncached resource.
64 | func WithWarnOnUncachedReads(warn bool) Option {
65 | return func(options *Options) {
66 | options.WarnOnUncachedReads = warn
67 | }
68 | }
69 |
70 | // DefaultOptions returns default value of Options.
71 | func DefaultOptions() Options {
72 | return Options{
73 | ChangeRateLimit: rate.Inf,
74 | ChangeBurst: 0,
75 | MetricsEnabled: true,
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/bolt/namespaced.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package bolt
6 |
7 | import (
8 | "context"
9 | "fmt"
10 |
11 | "go.etcd.io/bbolt"
12 |
13 | "github.com/cosi-project/runtime/pkg/resource"
14 | "github.com/cosi-project/runtime/pkg/state/impl/inmem"
15 | )
16 |
17 | var _ inmem.BackingStore = (*NamespacedBackingStore)(nil)
18 |
19 | // NamespacedBackingStore implements inmem.BackingStore for a given namespace.
20 | type NamespacedBackingStore struct {
21 | store *BackingStore
22 | namespace resource.Namespace
23 | }
24 |
25 | // Put implements inmem.BackingStore.
26 | func (store *NamespacedBackingStore) Put(_ context.Context, resourceType resource.Type, res resource.Resource) error {
27 | marshaled, err := store.store.marshaler.MarshalResource(res)
28 | if err != nil {
29 | return err
30 | }
31 |
32 | return store.store.db.Update(func(tx *bbolt.Tx) error {
33 | bucket, err := tx.CreateBucketIfNotExists([]byte(store.namespace))
34 | if err != nil {
35 | return err
36 | }
37 |
38 | typeBucket, err := bucket.CreateBucketIfNotExists([]byte(resourceType))
39 | if err != nil {
40 | return err
41 | }
42 |
43 | return typeBucket.Put([]byte(res.Metadata().ID()), marshaled)
44 | })
45 | }
46 |
47 | // Destroy implements inmem.BackingStore.
48 | func (store *NamespacedBackingStore) Destroy(_ context.Context, resourceType resource.Type, ptr resource.Pointer) error {
49 | return store.store.db.Update(func(tx *bbolt.Tx) error {
50 | bucket, err := tx.CreateBucketIfNotExists([]byte(store.namespace))
51 | if err != nil {
52 | return err
53 | }
54 |
55 | typeBucket, err := bucket.CreateBucketIfNotExists([]byte(resourceType))
56 | if err != nil {
57 | return err
58 | }
59 |
60 | return typeBucket.Delete([]byte(ptr.ID()))
61 | })
62 | }
63 |
64 | // Load implements inmem.BackingStore.
65 | func (store *NamespacedBackingStore) Load(_ context.Context, handler inmem.LoadHandler) error {
66 | return store.store.db.View(func(tx *bbolt.Tx) error {
67 | bucket := tx.Bucket([]byte(store.namespace))
68 | if bucket == nil {
69 | return nil
70 | }
71 |
72 | return bucket.ForEach(func(typeKey, val []byte) error {
73 | if val != nil {
74 | return fmt.Errorf("expected only buckets, got value for key %v", string(typeKey))
75 | }
76 |
77 | typeBucket := bucket.Bucket(typeKey)
78 | resourceType := resource.Type(typeKey)
79 |
80 | return typeBucket.ForEach(func(_, marshaled []byte) error {
81 | res, err := store.store.marshaler.UnmarshalResource(marshaled)
82 | if err != nil {
83 | return err
84 | }
85 |
86 | return handler(resourceType, res)
87 | })
88 | })
89 | })
90 | }
91 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/metrics/state.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package metrics
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/cosi-project/runtime/pkg/resource"
11 | "github.com/cosi-project/runtime/pkg/state"
12 | )
13 |
14 | type metricsWrapper struct {
15 | innerState state.CoreState
16 | controllerName string
17 | }
18 |
19 | func (m *metricsWrapper) Get(ctx context.Context, pointer resource.Pointer, option ...state.GetOption) (resource.Resource, error) {
20 | ControllerReads.Add(m.controllerName, 1)
21 |
22 | return m.innerState.Get(ctx, pointer, option...)
23 | }
24 |
25 | func (m *metricsWrapper) List(ctx context.Context, kind resource.Kind, option ...state.ListOption) (resource.List, error) {
26 | ControllerReads.Add(m.controllerName, 1)
27 |
28 | return m.innerState.List(ctx, kind, option...)
29 | }
30 |
31 | func (m *metricsWrapper) Create(ctx context.Context, resource resource.Resource, option ...state.CreateOption) error {
32 | ControllerWrites.Add(m.controllerName, 1)
33 |
34 | return m.innerState.Create(ctx, resource, option...)
35 | }
36 |
37 | func (m *metricsWrapper) Update(ctx context.Context, newResource resource.Resource, opts ...state.UpdateOption) error {
38 | ControllerWrites.Add(m.controllerName, 1)
39 |
40 | return m.innerState.Update(ctx, newResource, opts...)
41 | }
42 |
43 | func (m *metricsWrapper) Destroy(ctx context.Context, pointer resource.Pointer, option ...state.DestroyOption) error {
44 | ControllerWrites.Add(m.controllerName, 1)
45 |
46 | return m.innerState.Destroy(ctx, pointer, option...)
47 | }
48 |
49 | func (m *metricsWrapper) Watch(ctx context.Context, pointer resource.Pointer, events chan<- state.Event, option ...state.WatchOption) error {
50 | ControllerReads.Add(m.controllerName, 1)
51 |
52 | return m.innerState.Watch(ctx, pointer, events, option...)
53 | }
54 |
55 | func (m *metricsWrapper) WatchKind(ctx context.Context, kind resource.Kind, events chan<- state.Event, option ...state.WatchKindOption) error {
56 | ControllerReads.Add(m.controllerName, 1)
57 |
58 | return m.innerState.WatchKind(ctx, kind, events, option...)
59 | }
60 |
61 | func (m *metricsWrapper) WatchKindAggregated(ctx context.Context, kind resource.Kind, c chan<- []state.Event, option ...state.WatchKindOption) error {
62 | ControllerReads.Add(m.controllerName, 1)
63 |
64 | return m.innerState.WatchKindAggregated(ctx, kind, c, option...)
65 | }
66 |
67 | // WrapState wraps state.State with metrics for the given controller name.
68 | func WrapState(controllerName string, st state.State) state.State {
69 | return state.WrapCore(&metricsWrapper{
70 | controllerName: controllerName,
71 | innerState: st,
72 | })
73 | }
74 |
--------------------------------------------------------------------------------
/pkg/controller/generic/transform/resource_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package transform_test
6 |
7 | import (
8 | "github.com/cosi-project/runtime/pkg/resource"
9 | "github.com/cosi-project/runtime/pkg/resource/meta"
10 | "github.com/cosi-project/runtime/pkg/resource/typed"
11 | )
12 |
13 | // ANamespaceName is the namespace of A resource.
14 | const ANamespaceName = resource.Namespace("ns-a")
15 |
16 | // AType is the type of A.
17 | const AType = resource.Type("A.test.cosi.dev")
18 |
19 | // A is a test resource.
20 | type A = typed.Resource[ASpec, AE]
21 |
22 | // NewA initializes a A resource.
23 | func NewA(id resource.ID, spec ASpec) *A {
24 | return typed.NewResource[ASpec, AE](
25 | resource.NewMetadata(ANamespaceName, AType, id, resource.VersionUndefined),
26 | spec,
27 | )
28 | }
29 |
30 | // AE provides auxiliary methods for A.
31 | type AE struct{}
32 |
33 | // ResourceDefinition implements core.ResourceDefinitionProvider interface.
34 | func (AE) ResourceDefinition() meta.ResourceDefinitionSpec {
35 | return meta.ResourceDefinitionSpec{
36 | Type: AType,
37 | DefaultNamespace: ANamespaceName,
38 | }
39 | }
40 |
41 | // ASpec provides A definition.
42 | type ASpec struct {
43 | Str string
44 | Int int
45 | }
46 |
47 | // DeepCopy generates a deep copy of NamespaceSpec.
48 | func (a ASpec) DeepCopy() ASpec {
49 | return a
50 | }
51 |
52 | // BNamespaceName is the namespace of B resource.
53 | const BNamespaceName = resource.Namespace("ns-b")
54 |
55 | // BType is the type of B.
56 | const BType = resource.Type("B.test.cosi.dev")
57 |
58 | // B is a test resource.
59 | type B = typed.Resource[BSpec, BE]
60 |
61 | // NewB initializes a B resource.
62 | func NewB(id resource.ID, spec BSpec) *B {
63 | return typed.NewResource[BSpec, BE](
64 | resource.NewMetadata(BNamespaceName, BType, id, resource.VersionUndefined),
65 | spec,
66 | )
67 | }
68 |
69 | // BE provides auxiliary methods for B.
70 | type BE struct{}
71 |
72 | // ResourceDefinition implements core.ResourceDefinitionProvider interface.
73 | func (BE) ResourceDefinition() meta.ResourceDefinitionSpec {
74 | return meta.ResourceDefinitionSpec{
75 | Type: BType,
76 | DefaultNamespace: BNamespaceName,
77 | }
78 | }
79 |
80 | // BSpec provides B definition.
81 | type BSpec struct {
82 | Out string
83 | TransformCount int
84 | }
85 |
86 | // DeepCopy generates a deep copy of BSpec.
87 | func (b BSpec) DeepCopy() BSpec {
88 | return b
89 | }
90 |
91 | var (
92 | _ resource.Resource = &A{}
93 | _ resource.Resource = &B{}
94 | _ meta.ResourceDefinitionProvider = &A{}
95 | _ meta.ResourceDefinitionProvider = &B{}
96 | )
97 |
--------------------------------------------------------------------------------
/pkg/resource/meta/resource_definition.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package meta
6 |
7 | import (
8 | "fmt"
9 |
10 | "github.com/siderolabs/gen/ensure"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | "github.com/cosi-project/runtime/pkg/resource/meta/spec"
14 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
15 | "github.com/cosi-project/runtime/pkg/resource/typed"
16 | )
17 |
18 | // ResourceDefinitionType is the type of ResourceDefinition.
19 | const ResourceDefinitionType = resource.Type("ResourceDefinitions.meta.cosi.dev")
20 |
21 | type (
22 | // PrintColumn describes extra columns to print for the resources.
23 | PrintColumn = spec.PrintColumn
24 |
25 | // ResourceDefinitionSpec provides ResourceDefinition definition.
26 | ResourceDefinitionSpec = spec.ResourceDefinitionSpec
27 |
28 | // ResourceDefinition provides metadata about namespaces.
29 | ResourceDefinition = typed.Resource[ResourceDefinitionSpec, ResourceDefinitionExtension]
30 | )
31 |
32 | // NewResourceDefinition initializes a ResourceDefinition resource.
33 | func NewResourceDefinition(spec ResourceDefinitionSpec) (*ResourceDefinition, error) {
34 | if err := spec.Fill(); err != nil {
35 | return nil, fmt.Errorf("error validating resource definition %q: %w", spec.Type, err)
36 | }
37 |
38 | return typed.NewResource[ResourceDefinitionSpec, ResourceDefinitionExtension](
39 | resource.NewMetadata(NamespaceName, ResourceDefinitionType, spec.ID(), resource.VersionUndefined),
40 | spec,
41 | ), nil
42 | }
43 |
44 | // ResourceDefinitionExtension provides auxiliary methods for ResourceDefinition.
45 | type ResourceDefinitionExtension struct{}
46 |
47 | // ResourceDefinition implements core.ResourceDefinitionProvider interface.
48 | func (ResourceDefinitionExtension) ResourceDefinition() ResourceDefinitionSpec {
49 | return ResourceDefinitionSpec{
50 | Type: ResourceDefinitionType,
51 | Aliases: []resource.Type{"api-resources"},
52 | DefaultNamespace: NamespaceName,
53 | PrintColumns: []PrintColumn{
54 | {
55 | Name: "Aliases",
56 | JSONPath: "{.aliases[:]}",
57 | },
58 | },
59 | }
60 | }
61 |
62 | // ResourceDefinitionProvider is implemented by resources which can be registered automatically.
63 | type ResourceDefinitionProvider interface {
64 | ResourceDefinition() ResourceDefinitionSpec
65 | }
66 |
67 | // ResourceWithRD is a resource providing resource definition.
68 | //
69 | // ResourceWithRD allows to pull resource namespace and type from the RD.
70 | type ResourceWithRD interface {
71 | ResourceDefinitionProvider
72 | resource.Resource
73 | }
74 |
75 | func init() {
76 | ensure.NoError(protobuf.RegisterResource(ResourceDefinitionType, &ResourceDefinition{}))
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/safe/reader.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package safe
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/cosi-project/runtime/pkg/controller"
11 | "github.com/cosi-project/runtime/pkg/controller/generic"
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | "github.com/cosi-project/runtime/pkg/state"
14 | )
15 |
16 | // ReaderGet is a type safe wrapper around reader.Get.
17 | func ReaderGet[T resource.Resource](ctx context.Context, rdr controller.Reader, ptr resource.Pointer) (T, error) { //nolint:ireturn
18 | got, err := rdr.Get(ctx, ptr)
19 | if err != nil {
20 | var zero T
21 |
22 | return zero, err
23 | }
24 |
25 | result, ok := got.(T)
26 | if !ok {
27 | var zero T
28 |
29 | return zero, typeMismatchErr(result, got)
30 | }
31 |
32 | return result, nil
33 | }
34 |
35 | // ReaderGetByID is a type safe wrapper around reader.Get.
36 | func ReaderGetByID[T generic.ResourceWithRD](ctx context.Context, rdr controller.Reader, id resource.ID) (T, error) { //nolint:ireturn
37 | var r T
38 |
39 | md := resource.NewMetadata(
40 | r.ResourceDefinition().DefaultNamespace,
41 | r.ResourceDefinition().Type,
42 | id,
43 | resource.VersionUndefined,
44 | )
45 |
46 | got, err := rdr.Get(ctx, md)
47 | if err != nil {
48 | var zero T
49 |
50 | return zero, err
51 | }
52 |
53 | result, ok := got.(T)
54 | if !ok {
55 | var zero T
56 |
57 | return zero, typeMismatchErr(result, got)
58 | }
59 |
60 | return result, nil
61 | }
62 |
63 | // ReaderList is a type safe wrapper around Reader.List.
64 | func ReaderList[T resource.Resource](ctx context.Context, rdr controller.Reader, kind resource.Kind, opts ...state.ListOption) (List[T], error) {
65 | got, err := rdr.List(ctx, kind, opts...)
66 | if err != nil {
67 | var zero List[T]
68 |
69 | return zero, err
70 | }
71 |
72 | if len(got.Items) == 0 {
73 | var zero List[T]
74 |
75 | return zero, nil
76 | }
77 |
78 | // Early assertion to make sure we don't have a type mismatch.
79 | if firstElExpected, ok := got.Items[0].(T); !ok {
80 | var zero List[T]
81 |
82 | return zero, typeMismatchFirstElErr(firstElExpected, got.Items[0])
83 | }
84 |
85 | return NewList[T](got), nil
86 | }
87 |
88 | // ReaderListAll is a type safe wrapper around Reader.List that uses default namaespace and type from ResourceDefinitionProvider.
89 | func ReaderListAll[T generic.ResourceWithRD](ctx context.Context, rdr controller.Reader, opts ...state.ListOption) (List[T], error) {
90 | var r T
91 |
92 | md := resource.NewMetadata(
93 | r.ResourceDefinition().DefaultNamespace,
94 | r.ResourceDefinition().Type,
95 | "",
96 | resource.VersionUndefined,
97 | )
98 |
99 | return ReaderList[T](ctx, rdr, md, opts...)
100 | }
101 |
--------------------------------------------------------------------------------
/pkg/task/task.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package task implements generic controller tasks running in goroutines.
6 | package task
7 |
8 | import (
9 | "context"
10 | "fmt"
11 | "sync"
12 | "time"
13 |
14 | "github.com/cenkalti/backoff/v4"
15 | "go.uber.org/zap"
16 | )
17 |
18 | // ID is a task ID.
19 | type ID = string
20 |
21 | // Spec configures a task.
22 | type Spec[T any] interface {
23 | ID() ID
24 | RunTask(ctx context.Context, logger *zap.Logger, in T) error
25 | }
26 |
27 | // Task is a generic controller task that can run in a goroutine with restarts and panic handling.
28 | type Task[T any, S Spec[T]] struct {
29 | spec S
30 | in T
31 |
32 | logger *zap.Logger
33 | cancel context.CancelFunc
34 | wg sync.WaitGroup
35 | }
36 |
37 | // New creates a new task.
38 | func New[T any, S Spec[T]](logger *zap.Logger, spec S, in T) *Task[T, S] {
39 | return &Task[T, S]{
40 | spec: spec,
41 | in: in,
42 | logger: logger.With(zap.String("task", spec.ID())),
43 | }
44 | }
45 |
46 | // Spec returns the task spec.
47 | func (task *Task[T, S]) Spec() S {
48 | return task.spec
49 | }
50 |
51 | // Start the task in a separate goroutine.
52 | func (task *Task[T, S]) Start(ctx context.Context) {
53 | task.wg.Add(1)
54 |
55 | ctx, task.cancel = context.WithCancel(ctx)
56 |
57 | go func() {
58 | defer task.wg.Done()
59 |
60 | task.runWithRestarts(ctx)
61 | }()
62 | }
63 |
64 | func (task *Task[T, S]) runWithRestarts(ctx context.Context) {
65 | backoff := backoff.NewExponentialBackOff()
66 |
67 | // disable number of retries limit
68 | backoff.MaxElapsedTime = 0
69 |
70 | for ctx.Err() == nil {
71 | err := task.runWithPanicHandler(ctx)
72 |
73 | // finished without an error
74 | if err == nil {
75 | task.logger.Info("task finished")
76 |
77 | return
78 | }
79 |
80 | interval := backoff.NextBackOff()
81 |
82 | task.logger.Error("restarting task", zap.Duration("interval", interval), zap.Error(err))
83 |
84 | select {
85 | case <-ctx.Done():
86 | return
87 | case <-time.After(interval):
88 | }
89 | }
90 | }
91 |
92 | func (task *Task[T, S]) runWithPanicHandler(ctx context.Context) (err error) { //nolint:nonamedreturns
93 | defer func() {
94 | if p := recover(); p != nil {
95 | err = fmt.Errorf("panic: %v", p)
96 |
97 | task.logger.Error("task panicked", zap.Stack("stack"), zap.Error(err))
98 | }
99 | }()
100 |
101 | return task.spec.RunTask(ctx, task.logger, task.in)
102 | }
103 |
104 | // Stop the task waiting for it to finish.
105 | func (task *Task[T, S]) Stop() {
106 | task.cancel()
107 |
108 | task.wg.Wait()
109 | }
110 |
111 | // EqualSpec is like [Spec] but it requires an Equal method from the spec.
112 | type EqualSpec[T any, S Spec[T]] interface {
113 | Spec[T]
114 | Equal(S) bool
115 | }
116 |
--------------------------------------------------------------------------------
/pkg/state/impl/store/bolt/bbolt_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package bolt_test
6 |
7 | import (
8 | "path/filepath"
9 | "testing"
10 |
11 | "github.com/siderolabs/gen/ensure"
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/require"
14 | "go.etcd.io/bbolt"
15 |
16 | "github.com/cosi-project/runtime/pkg/resource"
17 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
18 | "github.com/cosi-project/runtime/pkg/state/conformance"
19 | "github.com/cosi-project/runtime/pkg/state/impl/store"
20 | "github.com/cosi-project/runtime/pkg/state/impl/store/bolt"
21 | )
22 |
23 | func init() {
24 | ensure.NoError(protobuf.RegisterResource(conformance.PathResourceType, &conformance.PathResource{}))
25 | }
26 |
27 | func TestBboltStore(t *testing.T) { //nolint:tparallel
28 | t.Parallel()
29 |
30 | tmpDir := t.TempDir()
31 |
32 | marshaler := store.ProtobufMarshaler{}
33 |
34 | store, err := bolt.NewBackingStore(
35 | func() (*bbolt.DB, error) {
36 | return bbolt.Open(filepath.Join(tmpDir, "test.db"), 0o600, nil)
37 | },
38 | marshaler,
39 | )
40 | require.NoError(t, err)
41 |
42 | t.Cleanup(func() {
43 | assert.NoError(t, store.Close())
44 | })
45 |
46 | path1 := conformance.NewPathResource("ns1", "var/run1")
47 | path2 := conformance.NewPathResource("ns1", "var/run2")
48 | path3 := conformance.NewPathResource("ns2", "var/run3")
49 |
50 | t.Run("Fill", func(t *testing.T) {
51 | require.NoError(t, store.WithNamespace(path1.Metadata().Namespace()).Put(t.Context(), path1.Metadata().Type(), path1))
52 | require.NoError(t, store.WithNamespace(path2.Metadata().Namespace()).Put(t.Context(), path2.Metadata().Type(), path2))
53 | require.NoError(t, store.WithNamespace(path2.Metadata().Namespace()).Put(t.Context(), path2.Metadata().Type(), path2))
54 | require.NoError(t, store.WithNamespace(path3.Metadata().Namespace()).Put(t.Context(), path3.Metadata().Type(), path3))
55 | })
56 |
57 | t.Run("Remove", func(t *testing.T) {
58 | require.NoError(t, store.WithNamespace(path1.Metadata().Namespace()).Destroy(t.Context(), path1.Metadata().Type(), path1.Metadata()))
59 | })
60 |
61 | t.Run("Load", func(t *testing.T) {
62 | var resources []resource.Resource
63 |
64 | require.NoError(t, store.WithNamespace(path1.Metadata().Namespace()).Load(t.Context(), func(_ resource.Type, resource resource.Resource) error {
65 | resources = append(resources, resource)
66 |
67 | return nil
68 | }))
69 |
70 | require.Len(t, resources, 1)
71 | assert.True(t, resource.Equal(path2, resources[0]))
72 |
73 | resources = nil
74 |
75 | require.NoError(t, store.WithNamespace(path3.Metadata().Namespace()).Load(t.Context(), func(_ resource.Type, resource resource.Resource) error {
76 | resources = append(resources, resource)
77 |
78 | return nil
79 | }))
80 |
81 | require.Len(t, resources, 1)
82 | assert.True(t, resource.Equal(path3, resources[0]))
83 | })
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/controller/runtime/internal/qruntime/internal/queue/queue_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package queue_test
6 |
7 | import (
8 | "context"
9 | "fmt"
10 | "sync"
11 | "testing"
12 | "time"
13 |
14 | "github.com/stretchr/testify/assert"
15 | "github.com/stretchr/testify/require"
16 | "golang.org/x/sync/errgroup"
17 |
18 | "github.com/cosi-project/runtime/pkg/controller/runtime/internal/qruntime/internal/queue"
19 | )
20 |
21 | type itemTracker struct {
22 | processed map[int]int
23 | concurrent map[int]struct{}
24 | mu sync.Mutex
25 | }
26 |
27 | func (tracker *itemTracker) start(item int) error {
28 | tracker.mu.Lock()
29 | defer tracker.mu.Unlock()
30 |
31 | if _, present := tracker.concurrent[item]; present {
32 | return fmt.Errorf("duplicate item processing: %d", item)
33 | }
34 |
35 | tracker.concurrent[item] = struct{}{}
36 |
37 | return nil
38 | }
39 |
40 | func (tracker *itemTracker) doneWith(item int) {
41 | tracker.mu.Lock()
42 | tracker.processed[item]++
43 | delete(tracker.concurrent, item)
44 | tracker.mu.Unlock()
45 | }
46 |
47 | func TestQueue(t *testing.T) {
48 | q := queue.NewQueue[int, struct{}]()
49 |
50 | tracker := &itemTracker{
51 | processed: make(map[int]int),
52 | concurrent: make(map[int]struct{}),
53 | }
54 |
55 | const (
56 | numWorkers = 5
57 | numItems = 100
58 | numIterations = 100
59 | )
60 |
61 | ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
62 | t.Cleanup(cancel)
63 |
64 | now := time.Now()
65 |
66 | eg, ctx := errgroup.WithContext(ctx)
67 |
68 | eg.Go(func() error {
69 | q.Run(ctx)
70 |
71 | return nil
72 | })
73 |
74 | for i := range numWorkers {
75 | eg.Go(func() error {
76 | for {
77 | select {
78 | case <-ctx.Done():
79 | return nil
80 | case item := <-q.Get():
81 | if err := tracker.start(item.Key()); err != nil {
82 | item.Release()
83 |
84 | return err
85 | }
86 |
87 | time.Sleep(5 * time.Millisecond)
88 |
89 | tracker.doneWith(item.Key())
90 |
91 | if i%2 == 0 {
92 | item.Requeue(time.Now().Add(10 * time.Millisecond))
93 | } else {
94 | item.Release()
95 | }
96 | }
97 | }
98 | })
99 | }
100 |
101 | for range numIterations {
102 | for i := range numItems {
103 | q.Put(i, struct{}{})
104 |
105 | time.Sleep(time.Millisecond)
106 | }
107 | }
108 |
109 | // wait for the queue to be empty
110 | waitLoop:
111 | for {
112 | select {
113 | case <-time.After(time.Second):
114 | break waitLoop
115 | case item := <-q.Get():
116 | item.Requeue(now)
117 | }
118 | }
119 |
120 | cancel()
121 |
122 | require.NoError(t, eg.Wait())
123 |
124 | assert.Equal(t, int64(0), q.Len())
125 |
126 | for i := range numItems {
127 | assert.GreaterOrEqual(t, tracker.processed[i], 50)
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/pkg/resource/handle/handle.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package handle provides a way to wrap "handle/descriptor-like" resources. That is, for this resource
6 | // any sort of unmarsahling is not possible, but the user should define a way to marshal one into the yaml
7 | // representation and can define equality checks.
8 | package handle
9 |
10 | import (
11 | "errors"
12 | "reflect"
13 |
14 | "go.yaml.in/yaml/v4"
15 | )
16 |
17 | // Spec should be yaml.Marshaler.
18 | type Spec interface {
19 | yaml.Marshaler
20 | }
21 |
22 | // ResourceSpec wraps "handle-like" structures and adds DeepCopy and marshaling methods.
23 | type ResourceSpec[S Spec] struct {
24 | Value S
25 | }
26 |
27 | // MarshalYAML implements yaml.Marshaler interface. It calls MarshalYAML on the wrapped object.
28 | func (spec *ResourceSpec[S]) MarshalYAML() (any, error) { return spec.Value.MarshalYAML() }
29 |
30 | // DeepCopy implemenents DeepCopyable without actually copying the object sine there is no way to actually do this.
31 | func (spec ResourceSpec[S]) DeepCopy() ResourceSpec[S] { return spec }
32 |
33 | // MarshalJSON implements json.Marshaler.
34 | func (spec *ResourceSpec[S]) MarshalJSON() ([]byte, error) {
35 | return nil, errors.New("cannot marshal handle resource into the json")
36 | }
37 |
38 | // MarshalProto implements ProtoMarshaler.
39 | func (spec *ResourceSpec[S]) MarshalProto() ([]byte, error) {
40 | return nil, nil
41 | }
42 |
43 | // UnmarshalYAML implements yaml.Unmarshaler interface. Since we cannot unmarshal the object, we just return an error.
44 | func (spec *ResourceSpec[S]) UnmarshalYAML(*yaml.Node) error {
45 | return errors.New("cannot unmarshal handle resource from the yaml")
46 | }
47 |
48 | // UnmarshalJSON implements json.Unmarshaler.
49 | func (spec *ResourceSpec[S]) UnmarshalJSON([]byte) error {
50 | return errors.New("cannot unmarshal handle resource from the json")
51 | }
52 |
53 | // UnmarshalProto implements protobuf.ResourceUnmarshaler.
54 | func (spec *ResourceSpec[S]) UnmarshalProto([]byte) error {
55 | return errors.New("cannot unmarshal handle resource from the protobuf")
56 | }
57 |
58 | // Equal implements spec equality check.
59 | func (spec *ResourceSpec[S]) Equal(other any) bool {
60 | otherSpec, ok := other.(*ResourceSpec[S])
61 | if !ok {
62 | return false
63 | }
64 |
65 | if isSamePtr(spec.Value, otherSpec.Value) {
66 | return true
67 | }
68 |
69 | eq, ok := any(spec.Value).(interface {
70 | Equal(other S) bool
71 | })
72 | if !ok {
73 | return false
74 | }
75 |
76 | return eq.Equal(otherSpec.Value)
77 | }
78 |
79 | // equalPtr is equality check function for cases where S is a pointer.
80 | //
81 | // Starting from Go 1.21 [reflect.ValueOf] no longer escapes for most cases.
82 | func isSamePtr[S any](a, b S) bool {
83 | ar := reflect.ValueOf(a)
84 |
85 | if ar.Kind() != reflect.Pointer {
86 | // Not pointers so not equal.
87 | return false
88 | }
89 |
90 | // Point to the same location.
91 | return ar.Pointer() == reflect.ValueOf(b).Pointer()
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/controller/generic/destroy/destroy.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | // Package destroy provides a generic implementation of controller which cleans up tearing down resources without finalizers.
6 | package destroy
7 |
8 | import (
9 | "context"
10 | "fmt"
11 |
12 | "github.com/siderolabs/gen/optional"
13 | "go.uber.org/zap"
14 |
15 | "github.com/cosi-project/runtime/pkg/controller"
16 | "github.com/cosi-project/runtime/pkg/controller/generic"
17 | "github.com/cosi-project/runtime/pkg/resource"
18 | "github.com/cosi-project/runtime/pkg/safe"
19 | "github.com/cosi-project/runtime/pkg/state"
20 | )
21 |
22 | // Controller provides a generic implementation of a QController which destroys tearing down resources without finalizers.
23 | type Controller[Input generic.ResourceWithRD] struct {
24 | generic.NamedController
25 | concurrency optional.Optional[uint]
26 | }
27 |
28 | // NewController creates a new destroy Controller.
29 | func NewController[Input generic.ResourceWithRD](concurrency optional.Optional[uint]) *Controller[Input] {
30 | var input Input
31 |
32 | name := fmt.Sprintf("Destroy[%s]", input.ResourceDefinition().Type)
33 |
34 | return &Controller[Input]{
35 | concurrency: concurrency,
36 | NamedController: generic.NamedController{
37 | ControllerName: name,
38 | },
39 | }
40 | }
41 |
42 | // Settings implements controller.QController interface.
43 | func (ctrl *Controller[Input]) Settings() controller.QSettings {
44 | var input Input
45 |
46 | return controller.QSettings{
47 | Inputs: []controller.Input{
48 | {
49 | Namespace: input.ResourceDefinition().DefaultNamespace,
50 | Type: input.ResourceDefinition().Type,
51 | Kind: controller.InputQPrimary,
52 | },
53 | },
54 | Outputs: []controller.Output{
55 | {
56 | Type: input.ResourceDefinition().Type,
57 | Kind: controller.OutputShared,
58 | },
59 | },
60 | Concurrency: ctrl.concurrency,
61 | }
62 | }
63 |
64 | // Reconcile implements controller.QController interface.
65 | func (ctrl *Controller[Input]) Reconcile(ctx context.Context, logger *zap.Logger, r controller.QRuntime, ptr resource.Pointer) error {
66 | in, err := safe.ReaderGet[Input](ctx, r, ptr)
67 | if err != nil {
68 | if state.IsNotFoundError(err) {
69 | return nil
70 | }
71 |
72 | return fmt.Errorf("error reading input resource: %w", err)
73 | }
74 |
75 | // only handle tearing down resources
76 | if in.Metadata().Phase() != resource.PhaseTearingDown {
77 | return nil
78 | }
79 |
80 | // only destroy resources without owner
81 | if in.Metadata().Owner() != "" {
82 | return nil
83 | }
84 |
85 | // do not do anything while the resource has any finalizers
86 | if !in.Metadata().Finalizers().Empty() {
87 | return nil
88 | }
89 |
90 | logger.Info("destroy the resource without finalizers")
91 |
92 | return r.Destroy(ctx, in.Metadata(), controller.WithOwner(""))
93 | }
94 |
95 | // MapInput implements controller.QController interface.
96 | func (ctrl *Controller[Input]) MapInput(context.Context, *zap.Logger, controller.QRuntime, controller.ReducedResourceMetadata) ([]resource.Pointer, error) {
97 | return nil, nil
98 | }
99 |
--------------------------------------------------------------------------------
/pkg/keystorage/testdata/public.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PGP PUBLIC KEY BLOCK-----
2 | Version: GopenPGP 2.4.10
3 | Comment: https://gopenpgp.org
4 |
5 | xsFNBGMscC4BEAC76m7SzaTLyTdhA1wChiwSZrQGywcmvG5S8hKAQLqTdULzJpnw
6 | uPH9stsPPvDKCJpxx5n6OwL/vDiPj7sslLJtDi3jhexmvWBEgYovU3BY4Udxozmm
7 | jVb3KdMV5PvZ0zH5UNWEeUm30FLfsnBVctLKcCNgg0jTlBn8x/cWSa2OtJky7o3K
8 | 5Z3ahUnN7uWZ5gtrr5OPEFiNU4+4ZZGKvr3KqQOvoHa4V7ciPJ/rJZBNizY0TiXI
9 | 5YFSZ4B/k3tWWQSiJQX9fWTPugYGmSZfLqvB71VI5Wz6Dor4IfGIWKHBfhHO4NPh
10 | 9VXr9+SQki0jORhpg+4eZhLoFYV8R4kSDQplCC/+DP5zDYEclGYliQQTGON1UqL6
11 | 0XJrWPsLWc3ZxqKZVvhlzdNBME1SrRYGC/FETynuLG/W84tZxMnt/MX4dAuNx0WT
12 | SAVCzuG7igXBSp1xxzZfxV6SdwDsuVxrNTz3pIdckusnOHA084zmUdQu0cvUJ5Os
13 | 9h/ih9gwfnESIt2+OZSWczSxCob+OxF9ZoGVITJQrLu2KDbi7ochmMojW/8iUarI
14 | w1CVtPthfw6LKvpx0nf+1hi7bRULr+/JgNvU7GLDqKS03z6jG4dqUU4/9wwxYq5R
15 | dFOZ0hd0qc5QKhiSWx5+WxPc5HmuZe6+vE1axesV+FC5Q0QnUe7mKE7fWwARAQAB
16 | zR9zaWRlcm8gPGRlZmF1bHRAc2lkZXJvbGFicy5jb20+wsGKBBMBCAA+BQJjLHAu
17 | CZCau9Ss/e4cRhYhBBsHV7z1hfc18NW4lZq71Kz97hxGAhsDAh4BAhkBAwsJBwIV
18 | CAMWAAICIgEAAG7fD/0clE1AFKSjiT+iesTCR1s1UEGGnpsBpFRdyTEUQzqO2EQs
19 | 5btztglcmuOSuKVsoeJ2QDYFUTgEKoyawCh2T45oBw11z8ylLuH/3O8i1NUv9icw
20 | 6oW+QxbpQovoH6RzX/XrfodCONdtpoZ/UQxbf6PR5jOtTjU3vBiuHlnCTGdXVpv8
21 | MdTpPmqYPL3NG5Pc7aIc3MrxtHZD4g9SszW9UiCtgZ0+7WfMLYD1FL2rLsbaPDge
22 | jzscMlvSuwjwTyFpB8YRHXMIEeY9fVLsoadgO3Pw1gnHpJP6bBkmrK5aYShkp2eM
23 | BR8Td3UIhYV7tom73F3wBHZq52oRubdckyRmUx7JADS0QjIVAjIDC3bMlNMcnTW3
24 | XxSe6RNiexyD6ngnO227U6wtO8nAsqDWlZCnP5QzANI1Zu+8tyuXzPh0V67Vy5Hd
25 | QvK30pCICMkkVDfLYcjdYv1Yfv3zk+YOT0ID6qN8YIrtdAEmZzEYW/jywzRBTVhe
26 | 0S1MBEgbz09uue188Ub8DGMhjmP07kTyhW67ATtPzcTZn/bLSns3SG/We+s85X32
27 | qv9tAbYPftk9XiFydTImZYnXu6OsDtI0heJ29sHJslC35nbqPOBPFOPMPrzHndOE
28 | aRc7Mg1sOlIzSZvDqZbLdh6CbJPQt5Vw/5F+x9hTJlV/qNk0SIUVQ+yK0w0NCc7B
29 | TQRjLHAuARAA1zYZ0AMsGbi24I6lY5HNAEDfMs+tx9D6JsI2merwYwucuAlaWaWS
30 | Opnb9kWpNwDsvQjSQmaur2qGtDw+xLc3ZCjMNp+Rl6oCwMksy0pCgLWWt9AI2Yjt
31 | V/7tzjZTWgkNuGMeKSQdV1jEoHKQ1FU250rl/4i0JWbelW59Dbh6nTjMqeaO2WtH
32 | sa7ec9YqvFolzYMegH1urSNpPS70bzkI7u3oIyL1ZMQHn585U9yhqbGELf3gH33q
33 | BdZOC/CncOEFabu1mbYupszl64Ua/c3RIYWpv1kmoPfyVXZJWH+bJ0fav+mXD6RQ
34 | F/pwzLDl8ysSH37a7RYQ6h50dguHisNVJ0MYXosc/1KjYjCIxET/AHYFFQ3KVaeH
35 | JjGjMtUNF3aiaqO2PvjIN0icx2z6FQwqOHocRa4OCCdH9UgO5bApRMOisNXq59bF
36 | m9dOOmvtgkYqDEHB5VN8UR2i3XncZga89CofBoLpncl+78AtJYuH37UpwT8VG1Z5
37 | qB6Kr4Cs0E+GswsOrNMpr5sAftqekGrO72PxestpFIWV11jsf3FkxYqMTiChLO52
38 | /ttAjg76BnMJlmVEgMiYPkG7tv3qwC+I+gf+1wYKMMURSY8V0BFlXF+jM9aM0mXE
39 | aWymLAdYOtbrR3Fpvk057HLQ7oGnlMmK6WtjhAGxbn7Cy6wHmkUoGqcAEQEAAcLB
40 | dgQYAQgAKgUCYyxwLgmQmrvUrP3uHEYWIQQbB1e89YX3NfDVuJWau9Ss/e4cRgIb
41 | DAAA8xMP/jnqUgFL+Uduvt4QhHWoC4jFuJCpzy/gdbvn3m95DYx/sJwTyqu1zovW
42 | VGudX9LkiQfWRWfuFRS9W6oX86nCH5/iXjR0xpSLv+1tqz87Dj8ngMJH9+9dQZpE
43 | WvHoQ3va/d9mudPfHxMPe81FLWaoiYU9QB4S1rErSxwzVROuhWeaXwvORhUfwx6L
44 | W0rHMRaFAD8ULwFJGEhT+zik5BLzmlvZhMcQeu3tX0AV4q7S/iIw2BXwBf3lpaan
45 | /RFAboQN7k6NZwx4MGiGCkx5XuHu2IEiT6Vd4E5whxkY3vYtktK438+kVfPrEiFQ
46 | /iODER0j4TKMMqWEDlnPX0Gs2kCyIstbXCjbbI91UTPGy6oYv4iLMjTUnsyWAPa3
47 | RoRFWln0rzyBBdmrOckaKEhjAuL17Pdq7xfARateCSHcCa3gFJUEPcMwVk9Iakdv
48 | 6hgA4mctok9yG5e/cnoz1tje0y0nnVm3x059ILhGKHfj8pk/mwD9th6New8JD7Xg
49 | RuvYaaK5qcUf3SbT7cuiwC/tcYg1VsGtpLsIwLtQFR+gXaU5SLy9q+LToxm4WqOP
50 | 1PmaxQGxw6p3oswwAY7BCtLGiYIH5Bm8Q2L7lhq70FZoFKzpMVRyU7hmzRPJEJwx
51 | 2golOjh3qA1R94wl8dYBNLJvGL0xH5dlaBwqrezm4h+lGE9o01wX
52 | =3arf
53 | -----END PGP PUBLIC KEY BLOCK-----
54 |
--------------------------------------------------------------------------------
/pkg/state/impl/inmem/options.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package inmem
6 |
7 | // StateOptions configure inmem.State.
8 | type StateOptions struct {
9 | BackingStore BackingStore
10 | HistoryMaxCapacity int
11 | HistoryInitialCapacity int
12 | HistoryGap int
13 | }
14 |
15 | // StateOption applies settings to StateOptions.
16 | type StateOption func(options *StateOptions)
17 |
18 | // WithHistoryCapacity sets history depth for a given namspace and resource.
19 | //
20 | // Deprecated: use WithHistoryMaxCapacity and WithHistoryInitialCapacity instead.
21 | func WithHistoryCapacity(capacity int) StateOption {
22 | return func(options *StateOptions) {
23 | options.HistoryMaxCapacity = capacity
24 | options.HistoryInitialCapacity = capacity
25 | }
26 | }
27 |
28 | // WithHistoryMaxCapacity sets history depth for a given namspace and resource.
29 | //
30 | // Deep history requires more memory, but allows Watch request to return more historical entries, and also
31 | // acts like a buffer if watch consumer can't keep up with events.
32 | //
33 | // Max capacity limits the maximum depth of the history buffer.
34 | func WithHistoryMaxCapacity(maxCapacity int) StateOption {
35 | return func(options *StateOptions) {
36 | options.HistoryMaxCapacity = maxCapacity
37 |
38 | if options.HistoryInitialCapacity > options.HistoryMaxCapacity {
39 | options.HistoryInitialCapacity = options.HistoryMaxCapacity
40 | }
41 | }
42 | }
43 |
44 | // WithHistoryInitialCapacity sets initial history depth for a given namspace and resource.
45 | //
46 | // Deep history requires more memory, but allows Watch request to return more historical entries, and also
47 | // acts like a buffer if watch consumer can't keep up with events.
48 | //
49 | // Initial capacity of the history buffer is used at the creation time and grows to the max capacity
50 | // based on the number of events.
51 | func WithHistoryInitialCapacity(initialCapacity int) StateOption {
52 | return func(options *StateOptions) {
53 | options.HistoryInitialCapacity = initialCapacity
54 |
55 | if options.HistoryMaxCapacity < options.HistoryInitialCapacity {
56 | options.HistoryMaxCapacity = options.HistoryInitialCapacity
57 | }
58 | }
59 | }
60 |
61 | // WithHistoryGap sets a safety gap between watch events consumers and events producers.
62 | //
63 | // Bigger gap reduces effective history depth (HistoryCapacity - HistoryGap).
64 | // Smaller gap might result in buffer overruns if consumer can't keep up with the events.
65 | // It's recommended to have gap 5% of the capacity.
66 | func WithHistoryGap(gap int) StateOption {
67 | return func(options *StateOptions) {
68 | options.HistoryGap = gap
69 | }
70 | }
71 |
72 | // WithBackingStore sets a BackingStore for a in-memory resource collection.
73 | //
74 | // Default value is nil (no backing store).
75 | func WithBackingStore(store BackingStore) StateOption {
76 | return func(options *StateOptions) {
77 | options.BackingStore = store
78 | }
79 | }
80 |
81 | // DefaultStateOptions returns default value of StateOptions.
82 | func DefaultStateOptions() StateOptions {
83 | return StateOptions{
84 | HistoryMaxCapacity: 100,
85 | HistoryInitialCapacity: 100,
86 | HistoryGap: 5,
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/controller/conformance/resources.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package conformance
6 |
7 | import (
8 | "encoding/binary"
9 |
10 | "github.com/cosi-project/runtime/pkg/resource"
11 | )
12 |
13 | // IntegerResource is implemented by resources holding ints.
14 | type IntegerResource interface {
15 | Value() int
16 | SetValue(int)
17 | }
18 |
19 | // StringResource is implemented by resources holding strings.
20 | type StringResource interface {
21 | Value() string
22 | SetValue(string)
23 | }
24 |
25 | // IntResourceType is the type of IntResource.
26 | const IntResourceType = resource.Type("test/int")
27 |
28 | // IntResource represents some integer value.
29 | type IntResource = Resource[int, intSpec, *intSpec]
30 |
31 | // NewIntResource creates new IntResource.
32 | func NewIntResource(ns resource.Namespace, id resource.ID, value int) *IntResource {
33 | return NewResource[int, intSpec, *intSpec](resource.NewMetadata(ns, IntResourceType, id, resource.VersionUndefined), value)
34 | }
35 |
36 | type intSpec struct{ ValueGetSet[int] } //nolint:recvcheck
37 |
38 | func (is *intSpec) FromProto(bytes []byte) {
39 | v, _ := binary.Varint(bytes)
40 | is.value = int(v)
41 | }
42 |
43 | func (is intSpec) MarshalProto() ([]byte, error) {
44 | buf := make([]byte, binary.MaxVarintLen64)
45 | n := binary.PutVarint(buf, int64(is.value))
46 |
47 | return buf[:n], nil
48 | }
49 |
50 | // StrResourceType is the type of StrResource.
51 | const StrResourceType = resource.Type("test/str")
52 |
53 | // StrResource represents some string value.
54 | type StrResource = Resource[string, strSpec, *strSpec]
55 |
56 | // NewStrResource creates new StrResource.
57 | func NewStrResource(ns resource.Namespace, id resource.ID, value string) *StrResource {
58 | return NewResource[string, strSpec, *strSpec](resource.NewMetadata(ns, StrResourceType, id, resource.VersionUndefined), value)
59 | }
60 |
61 | type strSpec struct{ ValueGetSet[string] } //nolint:recvcheck
62 |
63 | func (s *strSpec) FromProto(bytes []byte) { s.value = string(bytes) }
64 | func (s strSpec) MarshalProto() ([]byte, error) { return []byte(s.value), nil }
65 |
66 | // SentenceResourceType is the type of SentenceResource.
67 | const SentenceResourceType = resource.Type("test/sentence")
68 |
69 | // SentenceResource represents some string value.
70 | type SentenceResource = Resource[string, sentenceSpec, *sentenceSpec]
71 |
72 | // NewSentenceResource creates new SentenceResource.
73 | func NewSentenceResource(ns resource.Namespace, id resource.ID, value string) *SentenceResource {
74 | return NewResource[string, sentenceSpec, *sentenceSpec](resource.NewMetadata(ns, SentenceResourceType, id, resource.VersionUndefined), value)
75 | }
76 |
77 | type sentenceSpec struct{ ValueGetSet[string] } //nolint:recvcheck
78 |
79 | func (s *sentenceSpec) FromProto(bytes []byte) { s.value = string(bytes) }
80 | func (s sentenceSpec) MarshalProto() ([]byte, error) { return []byte(s.value), nil }
81 |
82 | // ValueGetSet is a basic building block for IntegerResource and StringResource implementations.
83 | type ValueGetSet[T any] struct{ value T }
84 |
85 | func (s *ValueGetSet[T]) SetValue(t T) { s.value = t } //nolint:revive
86 | func (s ValueGetSet[T]) Value() T { return s.value } //nolint:ireturn,revive
87 |
--------------------------------------------------------------------------------
/pkg/state/protobuf/runtime_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package protobuf_test
6 |
7 | import (
8 | "context"
9 | "testing"
10 | "time"
11 |
12 | "github.com/siderolabs/gen/ensure"
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/require"
15 | "go.uber.org/zap/zaptest"
16 |
17 | "github.com/cosi-project/runtime/api/v1alpha1"
18 | "github.com/cosi-project/runtime/pkg/controller/conformance"
19 | "github.com/cosi-project/runtime/pkg/controller/runtime"
20 | "github.com/cosi-project/runtime/pkg/future"
21 | "github.com/cosi-project/runtime/pkg/resource/protobuf"
22 | "github.com/cosi-project/runtime/pkg/state"
23 | "github.com/cosi-project/runtime/pkg/state/protobuf/client"
24 | )
25 |
26 | func init() {
27 | ensure.NoError(protobuf.RegisterResource(conformance.IntResourceType, &conformance.IntResource{}))
28 | ensure.NoError(protobuf.RegisterResource(conformance.StrResourceType, &conformance.StrResource{}))
29 | }
30 |
31 | func TestProtobufWatchRuntimeRestart(t *testing.T) {
32 | grpcConn, grpcServer, restartServer, _ := ProtobufSetup(t)
33 |
34 | stateClient := v1alpha1.NewStateClient(grpcConn)
35 |
36 | logger := zaptest.NewLogger(t)
37 |
38 | st := state.WrapCore(client.NewAdapter(stateClient,
39 | client.WithRetryLogger(logger),
40 | ))
41 |
42 | rt, err := runtime.NewRuntime(st, logger)
43 | require.NoError(t, err)
44 |
45 | ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
46 | t.Cleanup(cancel)
47 |
48 | ctx, errCh := future.GoContext(ctx, rt.Run)
49 |
50 | require.NoError(t, rt.RegisterController(&conformance.IntToStrController{
51 | SourceNamespace: "one",
52 | TargetNamespace: "default",
53 | }))
54 | require.NoError(t, rt.RegisterController(&conformance.IntDoublerController{
55 | SourceNamespace: "another",
56 | TargetNamespace: "default",
57 | }))
58 |
59 | require.NoError(t, st.Create(ctx, conformance.NewIntResource("one", "1", 1)))
60 | require.NoError(t, st.Create(ctx, conformance.NewIntResource("another", "4", 4)))
61 |
62 | // wait for controller to start up
63 | _, err = st.WatchFor(ctx, conformance.NewStrResource("default", "1", "1").Metadata(), state.WithEventTypes(state.Created))
64 | require.NoError(t, err)
65 | _, err = st.WatchFor(ctx, conformance.NewIntResource("default", "4", 8).Metadata(), state.WithEventTypes(state.Created))
66 | require.NoError(t, err)
67 |
68 | // abort the server, watch should enter retry loop
69 | grpcServer.Stop()
70 |
71 | select {
72 | case err = <-errCh:
73 | require.Fail(t, "runtime finished unexpectedly", "error: %v", err)
74 | case <-time.After(100 * time.Millisecond):
75 | }
76 |
77 | _ = restartServer()
78 |
79 | // now another resource
80 | require.EventuallyWithT(t, func(collectT *assert.CollectT) {
81 | asrt := assert.New(collectT)
82 |
83 | // the call might fail as the connection is re-established
84 | asrt.NoError(st.Create(ctx, conformance.NewIntResource("another", "2", 2)))
85 | }, time.Second, 10*time.Millisecond, "failed to create resource")
86 |
87 | // wait for controller to start up
88 | _, err = st.WatchFor(ctx, conformance.NewIntResource("default", "2", 4).Metadata(), state.WithEventTypes(state.Created))
89 | require.NoError(t, err)
90 |
91 | cancel()
92 |
93 | err = <-errCh
94 | require.NoError(t, err)
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/task/runner.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package task
6 |
7 | import (
8 | "context"
9 | "sync"
10 |
11 | "go.uber.org/zap"
12 | )
13 |
14 | // EqualityFunc is used to compare two task specs.
15 | type EqualityFunc[T any] func(x, y T) bool
16 |
17 | // Runner manages running tasks.
18 | type Runner[T any, S Spec[T]] struct {
19 | running map[ID]*Task[T, S]
20 | equalityFunc EqualityFunc[S]
21 | mu sync.Mutex
22 | }
23 |
24 | // NewRunner creates a new task runner.
25 | func NewRunner[T any, S Spec[T]](equalityFunc EqualityFunc[S]) *Runner[T, S] {
26 | if equalityFunc == nil {
27 | panic("equalityFunc must not be nil")
28 | }
29 |
30 | return &Runner[T, S]{
31 | running: make(map[ID]*Task[T, S]),
32 | equalityFunc: equalityFunc,
33 | }
34 | }
35 |
36 | // NewEqualRunner creates a new task runner from spec with Equal method.
37 | func NewEqualRunner[S EqualSpec[T, S], T any]() *Runner[T, S] {
38 | return NewRunner[T, S](func(x, y S) bool { return x.Equal(y) })
39 | }
40 |
41 | // Stop all running tasks.
42 | func (runner *Runner[T, S]) Stop() {
43 | for _, task := range runner.running {
44 | task.Stop()
45 | }
46 | }
47 |
48 | // StartTask starts a new task.
49 | func (runner *Runner[T, S]) StartTask(ctx context.Context, logger *zap.Logger, id string, spec S, task T) {
50 | runner.mu.Lock()
51 | defer runner.mu.Unlock()
52 |
53 | running, ok := runner.running[id]
54 |
55 | if ok {
56 | if runner.equalityFunc(spec, running.spec) {
57 | return
58 | }
59 |
60 | logger.Debug("replacing task", zap.String("task", id))
61 |
62 | runner.stopTask(id)
63 | }
64 |
65 | runner.running[id] = New(logger, spec, task)
66 |
67 | logger.Debug("starting task", zap.String("task", id))
68 | runner.running[id].Start(ctx)
69 | }
70 |
71 | // StopTask stop the running task.
72 | func (runner *Runner[T, S]) StopTask(logger *zap.Logger, id string) {
73 | runner.mu.Lock()
74 | defer runner.mu.Unlock()
75 |
76 | logger.Debug("stopping task", zap.String("task", id))
77 |
78 | runner.stopTask(id)
79 | }
80 |
81 | func (runner *Runner[T, S]) stopTask(id string) {
82 | if _, ok := runner.running[id]; !ok {
83 | return
84 | }
85 |
86 | runner.running[id].Stop()
87 | delete(runner.running, id)
88 | }
89 |
90 | // Reconcile running tasks.
91 | func (runner *Runner[T, S]) Reconcile(ctx context.Context, logger *zap.Logger, shouldRun map[ID]S, in T) {
92 | runner.mu.Lock()
93 | defer runner.mu.Unlock()
94 |
95 | // stop running tasks which shouldn't run
96 | for id := range runner.running {
97 | if _, exists := shouldRun[id]; !exists {
98 | logger.Debug("stopping task", zap.String("task", id))
99 |
100 | runner.stopTask(id)
101 | } else if !runner.equalityFunc(shouldRun[id], runner.running[id].Spec()) {
102 | logger.Debug("replacing task", zap.String("task", id))
103 |
104 | runner.stopTask(id)
105 | }
106 | }
107 |
108 | // start tasks which aren't running
109 | for id := range shouldRun {
110 | if _, exists := runner.running[id]; !exists {
111 | runner.running[id] = New(logger, shouldRun[id], in)
112 |
113 | logger.Debug("starting task", zap.String("task", id))
114 | runner.running[id].Start(ctx)
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/pkg/state/impl/inmem/errors.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package inmem
6 |
7 | import (
8 | "errors"
9 | "fmt"
10 |
11 | "github.com/cosi-project/runtime/pkg/resource"
12 | )
13 |
14 | //nolint:errname
15 | type eNotFound struct {
16 | error
17 | }
18 |
19 | func (eNotFound) NotFoundError() {}
20 |
21 | // ErrNotFound generates error compatible with state.ErrNotFound.
22 | func ErrNotFound(r resource.Pointer) error {
23 | return eNotFound{
24 | fmt.Errorf("resource %s doesn't exist", r),
25 | }
26 | }
27 |
28 | //nolint:errname
29 | type eConflict struct {
30 | error
31 | resource resource.Pointer
32 | }
33 |
34 | func (eConflict) ConflictError() {}
35 |
36 | func (e eConflict) GetResource() resource.Pointer {
37 | return e.resource
38 | }
39 |
40 | //nolint:errname
41 | type eOwnerConflict struct {
42 | eConflict
43 | }
44 |
45 | func (eOwnerConflict) OwnerConflictError() {}
46 |
47 | //nolint:errname
48 | type ePhaseConflict struct {
49 | eConflict
50 | }
51 |
52 | func (ePhaseConflict) PhaseConflictError() {}
53 |
54 | // ErrAlreadyExists generates error compatible with state.ErrConflict.
55 | func ErrAlreadyExists(r resource.Reference) error {
56 | return eConflict{
57 | error: fmt.Errorf("resource %s already exists", r),
58 | resource: r,
59 | }
60 | }
61 |
62 | // ErrVersionConflict generates error compatible with state.ErrConflict.
63 | func ErrVersionConflict(r resource.Reference, expected, found resource.Version) error {
64 | return eConflict{
65 | error: fmt.Errorf("resource %s update conflict: expected version %q, actual version %q", r, expected, found),
66 | }
67 | }
68 |
69 | // ErrUpdateSameVersion generates error compatible with state.ErrConflict.
70 | func ErrUpdateSameVersion(r resource.Reference, version resource.Version) error {
71 | return eConflict{
72 | error: fmt.Errorf("resource %s update conflict: same %q version for new and existing objects", r, version),
73 | resource: r,
74 | }
75 | }
76 |
77 | // ErrPendingFinalizers generates error compatible with state.ErrConflict.
78 | func ErrPendingFinalizers(r resource.Metadata) error {
79 | return eConflict{
80 | error: fmt.Errorf("resource %s has pending finalizers %s", r, r.Finalizers()),
81 | resource: r,
82 | }
83 | }
84 |
85 | // ErrOwnerConflict generates error compatible with state.ErrConflict.
86 | func ErrOwnerConflict(r resource.Reference, owner string) error {
87 | return eOwnerConflict{
88 | eConflict{
89 | error: fmt.Errorf("resource %s is owned by %q", r, owner),
90 | resource: r,
91 | },
92 | }
93 | }
94 |
95 | // ErrPhaseConflict generates error compatible with ErrConflict.
96 | func ErrPhaseConflict(r resource.Reference, expectedPhase resource.Phase) error {
97 | return ePhaseConflict{
98 | eConflict{
99 | error: fmt.Errorf("resource %s is not in phase %s", r, expectedPhase),
100 | resource: r,
101 | },
102 | }
103 | }
104 |
105 | //nolint:errname
106 | type eInvalidWatchBookmark struct {
107 | error
108 | }
109 |
110 | func (eInvalidWatchBookmark) InvalidWatchBookmarkError() {}
111 |
112 | // ErrInvalidWatchBookmark generates error compatible with state.ErrInvalidWatchBookmark.
113 | var ErrInvalidWatchBookmark = eInvalidWatchBookmark{
114 | errors.New("invalid watch bookmark"),
115 | }
116 |
--------------------------------------------------------------------------------
/pkg/resource/protobuf/spec.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package protobuf
6 |
7 | import (
8 | "encoding/json"
9 |
10 | "go.yaml.in/yaml/v4"
11 | "google.golang.org/protobuf/encoding/protojson"
12 | "google.golang.org/protobuf/proto"
13 | )
14 |
15 | // Spec should be proto.Message and pointer.
16 | type Spec[T any] interface {
17 | proto.Message
18 | *T
19 | }
20 |
21 | // ResourceSpec wraps proto.Message structures and adds DeepCopy and marshaling methods.
22 | // T is a protobuf generated structure.
23 | // S is a pointer to T.
24 | // Example usage:
25 | // type WrappedSpec = ResourceSpec[ProtoSpec, *ProtoSpec].
26 | type ResourceSpec[T any, S Spec[T]] struct {
27 | Value S
28 | }
29 |
30 | // MarshalYAML implements yaml.Marshaler interface. We want it to inline `Value` field, without
31 | // using `inline` tag.
32 | func (spec *ResourceSpec[T, S]) MarshalYAML() (any, error) {
33 | return spec.Value, nil
34 | }
35 |
36 | // DeepCopy creates a copy of the wrapped proto.Message.
37 | func (spec ResourceSpec[T, S]) DeepCopy() ResourceSpec[T, S] {
38 | if cast, ok := any(spec.Value).(interface{ CloneVT() S }); ok {
39 | return ResourceSpec[T, S]{
40 | Value: cast.CloneVT(),
41 | }
42 | }
43 |
44 | return ResourceSpec[T, S]{
45 | Value: proto.Clone(spec.Value).(S), //nolint:forcetypeassert,errcheck
46 | }
47 | }
48 |
49 | // MarshalJSON implements json.Marshaler.
50 | func (spec *ResourceSpec[T, S]) MarshalJSON() ([]byte, error) {
51 | return json.Marshal(spec.Value)
52 | }
53 |
54 | // MarshalProto implements ProtoMarshaler.
55 | func (spec *ResourceSpec[T, S]) MarshalProto() ([]byte, error) {
56 | return ProtoMarshal(spec.Value)
57 | }
58 |
59 | // UnmarshalYAML implements yaml.Unmarshaler interface. We want it to inline `Value` field, without
60 | // using `inline` tag.
61 | func (spec *ResourceSpec[T, S]) UnmarshalYAML(node *yaml.Node) error {
62 | if spec.Value == nil {
63 | spec.Value = S(new(T))
64 | }
65 |
66 | return node.Decode(spec.Value)
67 | }
68 |
69 | // UnmarshalJSON implements json.Unmarshaler.
70 | func (spec *ResourceSpec[T, S]) UnmarshalJSON(bytes []byte) error {
71 | spec.Value = new(T)
72 |
73 | if unmarshaler, ok := any(spec.Value).(json.Unmarshaler); ok {
74 | return unmarshaler.UnmarshalJSON(bytes)
75 | }
76 |
77 | opts := protojson.UnmarshalOptions{}
78 |
79 | return opts.Unmarshal(bytes, spec.Value)
80 | }
81 |
82 | // UnmarshalProto implements protobuf.ResourceUnmarshaler.
83 | func (spec *ResourceSpec[T, S]) UnmarshalProto(protoBytes []byte) error {
84 | spec.Value = new(T)
85 |
86 | return ProtoUnmarshal(protoBytes, spec.Value)
87 | }
88 |
89 | // GetValue returns wrapped protobuf object.
90 | func (spec *ResourceSpec[T, S]) GetValue() proto.Message { //nolint:ireturn
91 | return spec.Value
92 | }
93 |
94 | // Equal implements spec equality check.
95 | func (spec *ResourceSpec[T, S]) Equal(other any) bool {
96 | otherSpec, ok := other.(*ResourceSpec[T, S])
97 | if !ok {
98 | return false
99 | }
100 |
101 | return ProtoEqual(spec.Value, otherSpec.Value)
102 | }
103 |
104 | // NewResourceSpec creates new ResourceSpec[T, S].
105 | // T is a protobuf generated structure.
106 | // S is a pointer to T.
107 | func NewResourceSpec[T any, S Spec[T]](value S) ResourceSpec[T, S] {
108 | return ResourceSpec[T, S]{
109 | Value: value,
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/pkg/state/condition.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package state
6 |
7 | import (
8 | "github.com/cosi-project/runtime/pkg/resource"
9 | )
10 |
11 | // ResourceConditionFunc checks some condition on the resource.
12 | type ResourceConditionFunc func(resource.Resource) (bool, error)
13 |
14 | // WatchForCondition describes condition WatchFor is waiting for.
15 | type WatchForCondition struct {
16 | // If set, match only if func returns true.
17 | Condition ResourceConditionFunc
18 | // If set, wait for resource phase to be one of the specified.
19 | Phases []resource.Phase
20 | // If set, watch only for specified event types.
21 | EventTypes []EventType
22 | // If true, wait for the finalizers to empty
23 | FinalizersEmpty bool
24 | }
25 |
26 | // Matches checks whether event matches a condition.
27 | func (condition *WatchForCondition) Matches(event Event) (bool, error) {
28 | if condition.EventTypes != nil {
29 | matched := false
30 |
31 | for _, typ := range condition.EventTypes {
32 | if typ == event.Type {
33 | matched = true
34 |
35 | break
36 | }
37 | }
38 |
39 | if !matched {
40 | return false, nil
41 | }
42 | }
43 |
44 | // allow following checks rely on the resource being present
45 | if event.Resource == nil {
46 | return false, nil
47 | }
48 |
49 | if condition.Condition != nil {
50 | matched, err := condition.Condition(event.Resource)
51 | if err != nil {
52 | return false, err
53 | }
54 |
55 | if !matched {
56 | return false, nil
57 | }
58 | }
59 |
60 | if condition.FinalizersEmpty {
61 | if event.Type == Destroyed {
62 | return false, nil
63 | }
64 |
65 | if !event.Resource.Metadata().Finalizers().Empty() {
66 | return false, nil
67 | }
68 | }
69 |
70 | if condition.Phases != nil {
71 | matched := false
72 |
73 | for _, phase := range condition.Phases {
74 | if event.Resource.Metadata().Phase() == phase {
75 | matched = true
76 |
77 | break
78 | }
79 | }
80 |
81 | if !matched {
82 | return false, nil
83 | }
84 | }
85 |
86 | // no conditions denied the event, consider it matching
87 | return true, nil
88 | }
89 |
90 | // WatchForConditionFunc builds WatchForCondition.
91 | type WatchForConditionFunc func(*WatchForCondition) error
92 |
93 | // WithEventTypes watches for specified event types (one of).
94 | func WithEventTypes(types ...EventType) WatchForConditionFunc {
95 | return func(condition *WatchForCondition) error {
96 | condition.EventTypes = append(condition.EventTypes, types...)
97 |
98 | return nil
99 | }
100 | }
101 |
102 | // WithCondition for specified condition on the resource.
103 | func WithCondition(conditionFunc ResourceConditionFunc) WatchForConditionFunc {
104 | return func(condition *WatchForCondition) error {
105 | condition.Condition = conditionFunc
106 |
107 | return nil
108 | }
109 | }
110 |
111 | // WithFinalizerEmpty waits for the resource finalizers to be empty.
112 | func WithFinalizerEmpty() WatchForConditionFunc {
113 | return func(condition *WatchForCondition) error {
114 | condition.FinalizersEmpty = true
115 |
116 | return nil
117 | }
118 | }
119 |
120 | // WithPhases watches for specified resource phases.
121 | func WithPhases(phases ...resource.Phase) WatchForConditionFunc {
122 | return func(condition *WatchForCondition) error {
123 | condition.Phases = append(condition.Phases, phases...)
124 |
125 | return nil
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/pkg/resource/labels_test.go:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | package resource_test
6 |
7 | import (
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 |
12 | "github.com/cosi-project/runtime/pkg/resource"
13 | "github.com/cosi-project/runtime/pkg/resource/kvutils"
14 | )
15 |
16 | func TestLabels(t *testing.T) {
17 | var labels resource.Labels
18 |
19 | assert.True(t, labels.Empty())
20 |
21 | labels.Set("a", "b")
22 | assert.False(t, labels.Empty())
23 |
24 | v, ok := labels.Get("a")
25 | assert.True(t, ok)
26 | assert.Equal(t, "b", v)
27 |
28 | labelsCopy := labels
29 | labels.Set("c", "d")
30 |
31 | assert.False(t, labels.Equal(labelsCopy))
32 |
33 | v, ok = labels.Get("c")
34 | assert.True(t, ok)
35 | assert.Equal(t, "d", v)
36 |
37 | _, ok = labelsCopy.Get("c")
38 | assert.False(t, ok)
39 |
40 | labelsCopy2 := labels
41 | labelsCopy2.Set("a", "bb")
42 | assert.False(t, labels.Equal(labelsCopy2))
43 |
44 | labelsCopy3 := labels
45 | assert.True(t, labels.Equal(labelsCopy3))
46 |
47 | labelsCopy3.Set("a", "b")
48 | assert.True(t, labels.Equal(labelsCopy3))
49 |
50 | labelsCopy3.Delete("d")
51 | assert.True(t, labels.Equal(labelsCopy3))
52 |
53 | labelsCopy3.Delete("a")
54 | assert.False(t, labels.Equal(labelsCopy3))
55 |
56 | _, ok = labelsCopy3.Get("a")
57 | assert.False(t, ok)
58 |
59 | _, ok = labels.Get("a")
60 | assert.True(t, ok)
61 |
62 | var termTests resource.Labels
63 |
64 | assert.True(t, termTests.Matches(resource.LabelTerm{
65 | Key: "nope",
66 | Op: resource.LabelOpExists,
67 | Invert: true,
68 | }))
69 |
70 | assert.False(t, termTests.Matches(resource.LabelTerm{
71 | Key: "nope",
72 | Op: resource.LabelOpExists,
73 | }))
74 |
75 | termTests.Set("yes", "")
76 |
77 | assert.True(t, termTests.Matches(resource.LabelTerm{
78 | Key: "yes",
79 | Op: resource.LabelOpExists,
80 | }))
81 |
82 | assert.False(t, termTests.Matches(resource.LabelTerm{
83 | Key: "yes",
84 | Op: resource.LabelOpExists,
85 | Invert: true,
86 | }))
87 |
88 | termTests.Set("num", "100")
89 |
90 | assert.False(t, termTests.Matches(resource.LabelTerm{
91 | Key: "num",
92 | Op: resource.LabelOpLTNumeric,
93 | Value: []string{"NaN"},
94 | Invert: true,
95 | }))
96 |
97 | assert.False(t, termTests.Matches(resource.LabelTerm{
98 | Key: "num",
99 | Op: resource.LabelOpLTENumeric,
100 | Value: []string{"NaN"},
101 | Invert: true,
102 | }))
103 |
104 | assert.False(t, termTests.Matches(resource.LabelTerm{
105 | Key: "nm",
106 | Op: resource.LabelOpLTENumeric,
107 | Value: []string{"NaN"},
108 | Invert: true,
109 | }))
110 | }
111 |
112 | func TestLabelsDo(t *testing.T) {
113 | var src resource.Labels
114 |
115 | src.Set("a", "b")
116 | src.Set("c", "d")
117 |
118 | var dst resource.Labels
119 |
120 | dst.Do(func(temp kvutils.TempKV) {
121 | for key, val := range src.Raw() {
122 | temp.Set(key, val)
123 | }
124 | })
125 | assert.True(t, dst.Equal(src))
126 |
127 | src.Do(func(temp kvutils.TempKV) { temp.Delete("a") })
128 | assert.False(t, dst.Equal(src))
129 | assert.EqualValues(t, dst.Keys(), []string{"a", "c"})
130 |
131 | dst.Do(func(temp kvutils.TempKV) { temp.Delete("a") })
132 | assert.True(t, dst.Equal(src))
133 |
134 | src.Do(func(temp kvutils.TempKV) { temp.Set("a", "b") })
135 | assert.False(t, dst.Equal(src))
136 |
137 | dst.Do(func(temp kvutils.TempKV) { temp.Set("a", "b") })
138 | assert.True(t, dst.Equal(src))
139 | }
140 |
--------------------------------------------------------------------------------