-
Notifications
You must be signed in to change notification settings - Fork 0
feat: automatic core type generation #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
c6b7e0f
9029986
d2bc6f7
2da4981
ba45ee9
a372cfb
7bc6251
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,44 @@ | ||
| with-expecter: true | ||
| pkgname: mocks | ||
| packages: | ||
| github.com/openfga/api/proto/openfga/v1: | ||
| config: | ||
| dir: internal/subroutine/mocks | ||
| outpkg: mocks | ||
| filename: mock_OpenFGAServiceClient.go | ||
| interfaces: | ||
| OpenFGAServiceClient: | ||
|
|
||
| sigs.k8s.io/controller-runtime/pkg/client: | ||
| config: | ||
| dir: internal/subroutine/mocks | ||
| outpkg: mocks | ||
| filename: mock_Client.go | ||
| interfaces: | ||
| Client: | ||
|
|
||
| sigs.k8s.io/controller-runtime/pkg/manager: | ||
| config: | ||
| dir: internal/subroutine/mocks | ||
| filename: mock_CTRLManager.go | ||
| structname: CTRLManager | ||
| interfaces: | ||
| Manager: | ||
|
|
||
| sigs.k8s.io/multicluster-runtime/pkg/manager: | ||
| config: | ||
| dir: internal/subroutine/mocks | ||
| outpkg: mocks | ||
| filename: mock_Manager.go | ||
| interfaces: | ||
| Manager: | ||
|
|
||
| sigs.k8s.io/controller-runtime/pkg/cluster: | ||
| config: | ||
| dir: internal/subroutine/mocks | ||
| outpkg: mocks | ||
| filename: mock_Cluster.go | ||
| interfaces: | ||
| Cluster: | ||
|
|
||
| k8s.io/client-go/discovery: | ||
| config: | ||
| dir: internal/subroutine/mocks | ||
| filename: mock_DiscoveryInterface.go | ||
| interfaces: | ||
| Cluster: | ||
| DiscoveryInterface: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,10 +77,7 @@ tasks: | |
| KUBEBUILDER_ASSETS: | ||
| sh: $(pwd)/{{.LOCAL_BIN}}/setup-envtest use {{.ENVTEST_K8S_VERSION}} --bin-dir $(pwd)/{{.LOCAL_BIN}} -p path | ||
| GO111MODULE: on | ||
| PLATFORM_MESH_TOKEN: ${PLATFORM_MESH_TOKEN} | ||
| cmds: | ||
| - echo "https://openmfp:[email protected]" >> $HOME/.git-credentials | ||
| - git config --global url."https://${PLATFORM_MESH_TOKEN}@github.com/".insteadOf "https://github.com/" | ||
| - go test -count=1 ./... {{.ADDITIONAL_COMMAND_ARGS}} | ||
| test: | ||
| deps: [setup:envtest] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,12 @@ | ||
| package subroutine | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "fmt" | ||
| "net/url" | ||
| "strings" | ||
| "text/template" | ||
|
|
||
| openfgav1 "github.com/openfga/api/proto/openfga/v1" | ||
| language "github.com/openfga/language/pkg/go/transformer" | ||
|
|
@@ -12,26 +16,76 @@ import ( | |
| "github.com/platform-mesh/golang-commons/logger" | ||
| "github.com/platform-mesh/security-operator/api/v1alpha1" | ||
| "google.golang.org/protobuf/encoding/protojson" | ||
| apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/client-go/discovery" | ||
| "k8s.io/client-go/rest" | ||
| ctrl "sigs.k8s.io/controller-runtime" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/reconcile" | ||
| mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" | ||
| mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" | ||
| ) | ||
|
|
||
| const schemaVersion = "1.2" | ||
| const ( | ||
| schemaVersion = "1.2" | ||
| ) | ||
|
|
||
| var ( | ||
| priviledgedGroupVersions = []string{"rbac.authorization.k8s.io/v1"} | ||
| groupVersions = []string{"authentication.k8s.io/v1", "authorization.k8s.io/v1", "v1"} | ||
|
|
||
| priviledgedTemplate = template.Must(template.New("model").Parse(`module internal_core_types_{{ .Name }} | ||
|
|
||
| {{ if eq .Scope "Cluster" }} | ||
| extend type core_platform-mesh_io_account | ||
| relations | ||
| define create_{{ .Group }}_{{ .Name }}: owner | ||
| define list_{{ .Group }}_{{ .Name }}: member | ||
| define watch_{{ .Group }}_{{ .Name }}: member | ||
| {{ end }} | ||
|
|
||
| {{ if eq .Scope "Namespaced" }} | ||
| extend type core_namespace | ||
| relations | ||
| define create_{{ .Group }}_{{ .Name }}: owner | ||
| define list_{{ .Group }}_{{ .Name }}: member | ||
| define watch_{{ .Group }}_{{ .Name }}: member | ||
| {{ end }} | ||
|
|
||
| type {{ .Group }}_{{ .Singular }} | ||
| relations | ||
| define parent: [{{ if eq .Scope "Namespaced" }}core_namespace{{ else }}core_platform-mesh_io_account{{ end }}] | ||
| define member: [role#assignee] or owner or member from parent | ||
| define owner: [role#assignee] or owner from parent | ||
|
|
||
| define get: member | ||
| define update: owner | ||
| define delete: owner | ||
| define patch: owner | ||
| define watch: member | ||
|
|
||
| define manage_iam_roles: owner | ||
| define get_iam_roles: member | ||
| define get_iam_users: member | ||
| `)) | ||
aaronschweig marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
| type NewDiscoveryClientFunc func(cfg *rest.Config) discovery.DiscoveryInterface | ||
|
|
||
| type authorizationModelSubroutine struct { | ||
| fga openfgav1.OpenFGAServiceClient | ||
| mgr mcmanager.Manager | ||
| allClient client.Client | ||
| fga openfgav1.OpenFGAServiceClient | ||
| mgr mcmanager.Manager | ||
| allClient client.Client | ||
| newDiscoveryClientFunc NewDiscoveryClientFunc | ||
| } | ||
aaronschweig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| func NewAuthorizationModelSubroutine(fga openfgav1.OpenFGAServiceClient, mgr mcmanager.Manager, allClient client.Client, log *logger.Logger) *authorizationModelSubroutine { | ||
| func NewAuthorizationModelSubroutine(fga openfgav1.OpenFGAServiceClient, mgr mcmanager.Manager, allClient client.Client, newDiscoveryClientFunc NewDiscoveryClientFunc, log *logger.Logger) *authorizationModelSubroutine { | ||
| return &authorizationModelSubroutine{ | ||
| fga: fga, | ||
| mgr: mgr, | ||
| allClient: allClient, | ||
| fga: fga, | ||
| mgr: mgr, | ||
| allClient: allClient, | ||
| newDiscoveryClientFunc: newDiscoveryClientFunc, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -92,6 +146,38 @@ func (a *authorizationModelSubroutine) Process(ctx context.Context, instance run | |
| }) | ||
| } | ||
|
|
||
| if store.Name != "orgs" { | ||
| cfg := rest.CopyConfig(a.mgr.GetLocalManager().GetConfig()) | ||
|
|
||
| parsed, err := url.Parse(cfg.Host) | ||
| if err != nil { | ||
| log.Error().Err(err).Msg("unable to parse host from config") | ||
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
|
|
||
| parsed.Path, err = url.JoinPath("clusters", fmt.Sprintf("root:orgs:%s", store.Name)) | ||
| if err != nil { | ||
| log.Error().Err(err).Msg("unable to join path") | ||
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
|
|
||
| cfg.Host = parsed.String() | ||
|
|
||
| discoveryClient := a.newDiscoveryClientFunc(cfg) | ||
aaronschweig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| coreModules, err := discoverAndRender(discoveryClient, modelTpl, groupVersions) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix compilation error: undefined variable Line 168 references You need to define a template for non-privileged core types. Add this definition in the coreModelTemplate = template.Must(template.New("model").Parse(`module internal_core_types_{{ .Name }}
type {{ .Group }}_{{ .Singular }}
relations
define parent: [{{ if eq .Scope "Namespaced" }}core_namespace{{ else }}core_platform-mesh_io_account{{ end }}]
define member: [role#assignee] or owner or member from parent
define owner: [role#assignee] or owner from parent
define get: member
define update: member
define delete: member
define patch: member
define watch: member
define manage_iam_roles: owner
define get_iam_roles: member
define get_iam_users: member
`))Then update line 168: -coreModules, err := discoverAndRender(discoveryClient, modelTpl, groupVersions)
+coreModules, err := discoverAndRender(discoveryClient, coreModelTemplate, groupVersions)🤖 Prompt for AI Agents |
||
| if err != nil { | ||
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
| moduleFiles = append(moduleFiles, coreModules...) | ||
aaronschweig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| priviledgedModules, err := discoverAndRender(discoveryClient, priviledgedTemplate, priviledgedGroupVersions) | ||
| if err != nil { | ||
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
| moduleFiles = append(moduleFiles, priviledgedModules...) | ||
| } | ||
|
|
||
| authorizationModel, err := language.TransformModuleFilesToModel(moduleFiles, schemaVersion) | ||
| if err != nil { | ||
| log.Error().Err(err).Msg("unable to transform module files to model") | ||
|
|
@@ -109,33 +195,22 @@ func (a *authorizationModelSubroutine) Process(ctx context.Context, instance run | |
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
|
|
||
| // the following ignore comments are due to the fact, that its incredibly hard to setup a specific condition where one of the parsing methods would fail | ||
|
|
||
| // Compare Proto objects directly instead of DSL strings to avoid ordering issues | ||
| // The two models should be semantically equivalent even if DSL ordering differs | ||
| currentRaw, err := protojson.Marshal(res.AuthorizationModel) | ||
| if err != nil { // coverage-ignore | ||
| log.Error().Err(err).Msg("unable to marshal current model") | ||
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
|
|
||
| current, err := language.TransformJSONStringToDSL(string(currentRaw)) | ||
| if err != nil { // coverage-ignore | ||
| log.Error().Err(err).Msg("unable to transform current model to dsl") | ||
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
|
|
||
| desiredRaw, err := protojson.Marshal(authorizationModel) | ||
| if err != nil { // coverage-ignore | ||
| log.Error().Err(err).Msg("unable to marshal desired model") | ||
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
|
|
||
| desired, err := language.TransformJSONStringToDSL(string(desiredRaw)) | ||
| if err != nil { // coverage-ignore | ||
| log.Error().Err(err).Msg("unable to transform desired model to dsl") | ||
| return ctrl.Result{}, errors.NewOperatorError(err, true, false) | ||
| } | ||
|
|
||
| if *current == *desired { | ||
| // Compare JSON representations instead of DSL strings | ||
| if string(currentRaw) == string(desiredRaw) { | ||
| return ctrl.Result{}, nil | ||
| } | ||
|
|
||
|
|
@@ -156,3 +231,56 @@ func (a *authorizationModelSubroutine) Process(ctx context.Context, instance run | |
|
|
||
| return ctrl.Result{}, nil | ||
| } | ||
|
|
||
| func processAPIResourceIntoModel(resource metav1.APIResource, tpl *template.Template) (bytes.Buffer, error) { | ||
|
|
||
| scope := apiextensionsv1.ClusterScoped | ||
| if resource.Namespaced { | ||
| scope = apiextensionsv1.NamespaceScoped | ||
| } | ||
|
|
||
| group := "core" | ||
| if resource.Group != "" { | ||
| group = resource.Group | ||
| } | ||
|
|
||
| var buffer bytes.Buffer | ||
| err := tpl.Execute(&buffer, modelInput{ | ||
| Name: resource.Name, | ||
| Group: strings.ReplaceAll(group, ".", "_"), // TODO: group name length capping | ||
| Singular: resource.SingularName, | ||
| Scope: string(scope), | ||
| }) | ||
| if err != nil { | ||
| return buffer, err | ||
| } | ||
|
|
||
| return buffer, nil | ||
| } | ||
|
|
||
| func discoverAndRender(dc discovery.DiscoveryInterface, tpl *template.Template, groupVersions []string) ([]language.ModuleFile, error) { | ||
| var files []language.ModuleFile | ||
| for _, gv := range groupVersions { | ||
| resourceList, err := dc.ServerResourcesForGroupVersion(gv) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("discover resources for %s: %w", gv, err) | ||
| } | ||
|
|
||
| for _, apiRes := range resourceList.APIResources { | ||
| if strings.Contains(apiRes.Name, "/") { // skip subresources | ||
| continue | ||
| } | ||
|
|
||
| buf, err := processAPIResourceIntoModel(apiRes, tpl) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("process api resource %s in %s: %w", apiRes.Name, gv, err) | ||
| } | ||
|
|
||
| files = append(files, language.ModuleFile{ | ||
| Name: fmt.Sprintf("internal_core_types_%s.fga", apiRes.Name), | ||
| Contents: buf.String(), | ||
| }) | ||
| } | ||
| } | ||
| return files, nil | ||
| } | ||
|
Comment on lines
+261
to
+286
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include group in ModuleFile name to prevent collisions. Resources with the same plural name in different API groups will overwrite each other's module files. Modify - files = append(files, language.ModuleFile{
- Name: fmt.Sprintf("internal_core_types_%s.fga", apiRes.Name),
- Contents: buf.String(),
- })
+ group := "core"
+ if apiRes.Group != "" {
+ group = strings.ReplaceAll(apiRes.Group, ".", "_")
+ }
+ files = append(files, language.ModuleFile{
+ Name: fmt.Sprintf("internal_core_types_%s_%s.fga", group, apiRes.Name),
+ Contents: buf.String(),
+ })
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
count=1 is the default