Skip to content

Commit

Permalink
feat: add the opsrule module (#56)
Browse files Browse the repository at this point in the history
<!-- Thank you for contributing to Catalog!

Note: 

1. With pull requests:

    - Open your pull request against "main"
- Your pull request should have no more than three commits, if not you
should squash them.
- It should pass all tests in the available continuous integration
systems such as GitHub Actions.
    - You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the
README.

2. Please create an issue first to describe the problem.

We recommend that link the issue with the PR in the following question.
For more info, check https://kusionstack.io/docs/governance/contribute/
-->

## What type of PR is this?

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind chore
-->

## What this PR does / why we need it:

## Which issue(s) this PR fixes:

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related
issues/tests in a comment and do not use `Fixes`_*
-->

Fixes #

## Special notes for your reviewer:

### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires
additional action from users switching to the new release, include the
string "action required".

-->

```release-note

```

### Additional documentation e.g., design docs, usage docs, etc.:

<!--
Please use the following format for linking documentation:
- [Design]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->

```docs

```
  • Loading branch information
SparkYuan authored Mar 6, 2024
1 parent 3479979 commit cbe9e71
Show file tree
Hide file tree
Showing 6 changed files with 505 additions and 1 deletion.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ __kclcache__
workspace/

# kusion state from local testing
kusion_state.yaml
kusion_state.yaml

# local bin files
models/bin/
28 changes: 28 additions & 0 deletions models/generators/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
TEST?=$$(go list ./... | grep -v 'vendor')
###### chang variables below according to your own modules ###
NAMESPACE=kusionstack
NAME=opsrule
VERSION=v0.1.0
BINARY=../bin/kusion-module-${NAME}_${VERSION}

LOCAL_ARCH := $(shell uname -m)
ifeq ($(LOCAL_ARCH),x86_64)
GOARCH_LOCAL := amd64
else
GOARCH_LOCAL := $(LOCAL_ARCH)
endif
export GOOS_LOCAL := $(shell uname|tr 'A-Z' 'a-z')
export OS_ARCH ?= $(GOARCH_LOCAL)

default: install

build-darwin:
GOOS=darwin GOARCH=arm64 go build -o ${BINARY} ./${NAME}

install: build-darwin
# copy module binary to $KUSION_HOME. e.g. ~/.kusion/modules/kusionstack/opsrule/v0.1.0/darwin/arm64/kusion-module-opsrule_v0.1.0
mkdir -p ${KUSION_HOME}/modules/${NAMESPACE}/${NAME}/${VERSION}/${GOOS_LOCAL}/${OS_ARCH}
cp ${BINARY} ${KUSION_HOME}/modules/${NAMESPACE}/${NAME}/${VERSION}/${GOOS_LOCAL}/${OS_ARCH}

test:
TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 5m
124 changes: 124 additions & 0 deletions models/generators/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package generators

import (
"fmt"

"github.com/hashicorp/go-plugin"
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/runtime"
v1 "kusionstack.io/kusion/pkg/apis/core/v1"
"kusionstack.io/kusion/pkg/apis/core/v1/workload"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/modules"
"kusionstack.io/kusion/pkg/modules/proto"
)

// HandshakeConfig is a common handshake that is shared by plugin and host.
var HandshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "MODULE_PLUGIN",
MagicCookieValue: "ON",
}

func StartModule(module modules.Module) {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: HandshakeConfig,
Plugins: map[string]plugin.Plugin{
modules.PluginKey: &modules.GRPCPlugin{Impl: module},
},

// A non-nil value here enables gRPC serving for this plugin...
GRPCServer: plugin.DefaultGRPCServer,
})
}

type GeneratorRequest struct {
// Project represents the project name
Project string `json:"project,omitempty"`
// Stack represents the stack name
Stack string `json:"stack,omitempty"`
// App represents the application name, which is typically the same as the namespace of Kubernetes resources
App string `json:"app,omitempty"`
// Workload represents the workload configuration
Workload *workload.Workload `json:"workload,omitempty"`
// DevModuleConfig is the developer's inputs of this module
DevModuleConfig v1.Accessory `json:"dev_module_config,omitempty"`
// PlatformModuleConfig is the platform engineer's inputs of this module
PlatformModuleConfig v1.GenericConfig `json:"platform_module_config,omitempty"`
// RuntimeConfig is the runtime configurations defined in the workspace config
RuntimeConfig *v1.RuntimeConfigs `json:"runtime_config,omitempty"`
}

func NewGeneratorRequest(req *proto.GeneratorRequest) (*GeneratorRequest, error) {

log.Infof("module proto request received:%s", req.String())

// validate workload
if req.Workload == nil {
return nil, fmt.Errorf("workload in the request is nil")
}
w := &workload.Workload{}
if err := yaml.Unmarshal(req.Workload, w); err != nil {
return nil, fmt.Errorf("unmarshal workload failed. %w", err)
}

var dc v1.Accessory
if req.DevModuleConfig != nil {
if err := yaml.Unmarshal(req.DevModuleConfig, &dc); err != nil {
return nil, fmt.Errorf("unmarshal dev module config failed. %w", err)
}
}

var pc v1.GenericConfig
if req.PlatformModuleConfig != nil {
if err := yaml.Unmarshal(req.PlatformModuleConfig, &pc); err != nil {
return nil, fmt.Errorf("unmarshal platform module config failed. %w", err)
}
}

var rc *v1.RuntimeConfigs
if req.RuntimeConfig != nil {
if err := yaml.Unmarshal(req.RuntimeConfig, rc); err != nil {
return nil, fmt.Errorf("unmarshal runtime config failed. %w", err)
}
}

result := &GeneratorRequest{
Project: req.Project,
Stack: req.Stack,
App: req.App,
Workload: w,
DevModuleConfig: dc,
PlatformModuleConfig: pc,
RuntimeConfig: rc,
}
out, err := yaml.Marshal(result)
if err != nil {
return nil, fmt.Errorf("marshal new generator request failed. %w", err)
}
log.Infof("new generator request:%s", string(out))
return result, nil
}

func EmptyResponse() *proto.GeneratorResponse {
return &proto.GeneratorResponse{}
}

func WrapK8sResourceToKusionResource(id string, resource any) (*v1.Resource, error) {
gvk := resource.(runtime.Object).GetObjectKind().GroupVersionKind().String()

// fixme: this function converts int to int64 by default
unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource)
if err != nil {
return nil, err
}
return &v1.Resource{
ID: id,
Type: v1.Kubernetes,
Attributes: unstructured,
DependsOn: nil,
Extensions: map[string]any{
v1.ResourceExtensionGVK: gvk,
},
}, nil
}
50 changes: 50 additions & 0 deletions models/generators/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module kusion-modules

go 1.19

require (
github.com/hashicorp/go-plugin v1.6.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.27.2
kusionstack.io/kube-api v0.1.1
kusionstack.io/kusion v0.10.1-0.20240305085131-a947ad87dfa8
)

require (
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/hashicorp/go-hclog v0.16.2 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.27.2 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/controller-runtime v0.15.1 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)
118 changes: 118 additions & 0 deletions models/generators/opsrule/opsrule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"errors"
"strconv"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"kusionstack.io/kube-api/apps/v1alpha1"
v1 "kusionstack.io/kusion/pkg/apis/core/v1"
"kusionstack.io/kusion/pkg/apis/core/v1/workload"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/modules"
"kusionstack.io/kusion/pkg/modules/proto"
jsonutil "kusionstack.io/kusion/pkg/util/json"

generators "kusion-modules"
)

type OpsRuleModule struct{}

func (o *OpsRuleModule) Generate(r *proto.GeneratorRequest) (*proto.GeneratorResponse, error) {
emptyResponse := generators.EmptyResponse()
request, err := generators.NewGeneratorRequest(r)
if err != nil {
return nil, err
}

// opsRule does not exist in AppConfig and workspace config
if request.DevModuleConfig == nil && request.PlatformModuleConfig == nil {
log.Info("OpsRule does not exist in AppConfig and workspace config")
return emptyResponse, nil
}

// Job does not support maxUnavailable
if request.Workload.Header.Type == workload.TypeJob {
log.Infof("Job does not support opsRule")
return emptyResponse, nil
}

if request.Workload.Service.Type == workload.Collaset {
maxUnavailable, err := GetMaxUnavailable(request.DevModuleConfig, request.PlatformModuleConfig)
if err != nil {
return nil, err
}
ptr := &v1alpha1.PodTransitionRule{
TypeMeta: metav1.TypeMeta{
APIVersion: v1alpha1.GroupVersion.String(),
Kind: "PodTransitionRule",
},
ObjectMeta: metav1.ObjectMeta{
Name: modules.UniqueAppName(request.Project, request.Stack, request.App),
Namespace: request.App,
},
Spec: v1alpha1.PodTransitionRuleSpec{
Selector: &metav1.LabelSelector{
MatchLabels: modules.UniqueAppLabels(request.Project, request.App),
},
Rules: []v1alpha1.TransitionRule{
{
Name: "maxUnavailable",
TransitionRuleDefinition: v1alpha1.TransitionRuleDefinition{
AvailablePolicy: &v1alpha1.AvailableRule{
MaxUnavailableValue: &maxUnavailable,
},
},
},
},
},
}
resourceID := modules.KubernetesResourceID(ptr.TypeMeta, ptr.ObjectMeta)
resource, err := generators.WrapK8sResourceToKusionResource(resourceID, ptr)
if err != nil {
return nil, err
}
str := jsonutil.Marshal2String(resource)
b := []byte(str)
return &proto.GeneratorResponse{
Resources: [][]byte{b},
}, nil
}
return emptyResponse, nil
}

func GetMaxUnavailable(devConfig v1.Accessory, platformConfig v1.GenericConfig) (intstr.IntOrString, error) {
var maxUnavailable interface{}
key := "maxUnavailable"

// developer config
// kusionstack/[email protected] : t.OpsRule {
// maxUnavailable: "30%"
// }
if devConfig != nil && devConfig[key] != "" {
maxUnavailable = devConfig[key]
} else if platformConfig == nil {
return intstr.IntOrString{}, nil
} else {
// platformConfig example
// kusionstack/[email protected]:
// maxUnavailable: 1 # or 10%
maxUnavailable = platformConfig[key]
}
var mu string
mu, isString := maxUnavailable.(string)
if !isString {
temp, isInt := maxUnavailable.(int)
if isInt {
mu = strconv.Itoa(temp)
} else {
return intstr.IntOrString{}, errors.New("illegal opsRule config. opsRule.maxUnavailable is not string or int")
}
}
return intstr.Parse(mu), nil
}

func main() {
modules.StartModule(&OpsRuleModule{})
}
Loading

0 comments on commit cbe9e71

Please sign in to comment.