mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: generate countries.tsx from Go code (#15274)
Closes https://github.com/coder/coder/issues/15074 We have a hard-coded list of countries at https://github.com/coder/coder/blob/main/site/src/pages/SetupPage/countries.tsx. This means Go code in coder/coder doesn't have an easy way of utilizing it. ## Solution Generate countries.tsx from Go code. Generated by `scripts/apitypings`
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
// Code generated by typegen/main.go. DO NOT EDIT.
|
||||
package codersdk
|
||||
|
||||
type RBACResource string
|
||||
|
||||
const (
|
||||
{{- range $element := . }}
|
||||
Resource{{ pascalCaseName $element.FunctionName }} RBACResource = "{{ $element.Type }}"
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
type RBACAction string
|
||||
|
||||
const (
|
||||
{{- range $element := actionsList }}
|
||||
{{ $element.Enum }} RBACAction = "{{ $element.Value }}"
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
// RBACResourceActions is the mapping of resources to which actions are valid for
|
||||
// said resource type.
|
||||
var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
{{- range $element := . }}
|
||||
Resource{{ pascalCaseName $element.FunctionName }}: {
|
||||
{{- range $actionValue, $_ := $element.Actions }}
|
||||
{{- actionEnum $actionValue -}},
|
||||
{{- end -}}
|
||||
},
|
||||
{{- end }}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Code generated by typegen/main.go. DO NOT EDIT.
|
||||
|
||||
// Countries represents all supported countries with their flags
|
||||
export const countries = [
|
||||
{{- range $country := . }}
|
||||
{
|
||||
name: "{{ $country.Name }}",
|
||||
flag: "{{ $country.Flag }}",
|
||||
},
|
||||
{{- end }}
|
||||
];
|
||||
@@ -0,0 +1,267 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
//go:embed rbacobject.gotmpl
|
||||
var rbacObjectTemplate string
|
||||
|
||||
//go:embed codersdk.gotmpl
|
||||
var codersdkTemplate string
|
||||
|
||||
//go:embed typescript.tstmpl
|
||||
var typescriptTemplate string
|
||||
|
||||
//go:embed countries.tstmpl
|
||||
var countriesTemplate string
|
||||
|
||||
func usage() {
|
||||
_, _ = fmt.Println("Usage: typegen <type> [template]")
|
||||
_, _ = fmt.Println("Types:")
|
||||
_, _ = fmt.Println(" rbac <object|codersdk|typescript> - Generate RBAC related files")
|
||||
_, _ = fmt.Println(" countries - Generate countries TypeScript")
|
||||
}
|
||||
|
||||
// main will generate a file based on the type and template specified.
|
||||
// This is to provide an "AllResources" function that is always
|
||||
// in sync.
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) < 1 {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var (
|
||||
out []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// It did not make sense to have 2 different generators that do essentially
|
||||
// the same thing, but different format for the BE and the sdk.
|
||||
// So the argument switches the go template to use.
|
||||
switch strings.ToLower(flag.Args()[0]) {
|
||||
case "rbac":
|
||||
if len(flag.Args()) < 2 {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
out, err = generateRBAC(flag.Args()[1])
|
||||
case "countries":
|
||||
out, err = generateCountries()
|
||||
default:
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%q is not a valid type\n", flag.Args()[0])
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Generate source: %s", err.Error())
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprint(os.Stdout, string(out))
|
||||
}
|
||||
|
||||
func generateRBAC(tmpl string) ([]byte, error) {
|
||||
formatSource := format.Source
|
||||
var source string
|
||||
switch strings.ToLower(tmpl) {
|
||||
case "codersdk":
|
||||
source = codersdkTemplate
|
||||
case "object":
|
||||
source = rbacObjectTemplate
|
||||
case "typescript":
|
||||
source = typescriptTemplate
|
||||
formatSource = func(src []byte) ([]byte, error) {
|
||||
// No typescript formatting
|
||||
return src, nil
|
||||
}
|
||||
default:
|
||||
return nil, xerrors.Errorf("%q is not a valid RBAC template target", tmpl)
|
||||
}
|
||||
|
||||
out, err := generateRbacObjects(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return formatSource(out)
|
||||
}
|
||||
|
||||
func generateCountries() ([]byte, error) {
|
||||
tmpl, err := template.New("countries.tstmpl").Parse(countriesTemplate)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse template: %w", err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
err = tmpl.Execute(&out, codersdk.Countries)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("execute template: %w", err)
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func pascalCaseName[T ~string](name T) string {
|
||||
names := strings.Split(string(name), "_")
|
||||
for i := range names {
|
||||
names[i] = capitalize(names[i])
|
||||
}
|
||||
return strings.Join(names, "")
|
||||
}
|
||||
|
||||
func capitalize(name string) string {
|
||||
return strings.ToUpper(string(name[0])) + name[1:]
|
||||
}
|
||||
|
||||
type Definition struct {
|
||||
policy.PermissionDefinition
|
||||
Type string
|
||||
}
|
||||
|
||||
func (p Definition) FunctionName() string {
|
||||
if p.Name != "" {
|
||||
return p.Name
|
||||
}
|
||||
return p.Type
|
||||
}
|
||||
|
||||
// fileActions is required because we cannot get the variable name of the enum
|
||||
// at runtime. So parse the package to get it. This is purely to ensure enum
|
||||
// names are consistent, which is a bit annoying, but not too bad.
|
||||
func fileActions(file *ast.File) map[string]string {
|
||||
// actions is a map from the enum value -> enum name
|
||||
actions := make(map[string]string)
|
||||
|
||||
// Find the action consts
|
||||
fileDeclLoop:
|
||||
for _, decl := range file.Decls {
|
||||
switch typedDecl := decl.(type) {
|
||||
case *ast.GenDecl:
|
||||
if len(typedDecl.Specs) == 0 {
|
||||
continue
|
||||
}
|
||||
// This is the right on, loop over all idents, pull the actions
|
||||
for _, spec := range typedDecl.Specs {
|
||||
vSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue fileDeclLoop
|
||||
}
|
||||
|
||||
typeIdent, ok := vSpec.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
continue fileDeclLoop
|
||||
}
|
||||
|
||||
if typeIdent.Name != "Action" || len(vSpec.Values) != 1 || len(vSpec.Names) != 1 {
|
||||
continue fileDeclLoop
|
||||
}
|
||||
|
||||
literal, ok := vSpec.Values[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
continue fileDeclLoop
|
||||
}
|
||||
actions[strings.Trim(literal.Value, `"`)] = vSpec.Names[0].Name
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
type ActionDetails struct {
|
||||
Enum string
|
||||
Value string
|
||||
}
|
||||
|
||||
// generateRbacObjects will take the policy.go file, and send it as input
|
||||
// to the go templates. Some AST of the Action enum is also included.
|
||||
func generateRbacObjects(templateSource string) ([]byte, error) {
|
||||
// Parse the policy.go file for the action enums
|
||||
f, err := parser.ParseFile(token.NewFileSet(), "./coderd/rbac/policy/policy.go", nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parsing policy.go: %w", err)
|
||||
}
|
||||
actionMap := fileActions(f)
|
||||
actionList := make([]ActionDetails, 0)
|
||||
for value, enum := range actionMap {
|
||||
actionList = append(actionList, ActionDetails{
|
||||
Enum: enum,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
// Sorting actions for auto gen consistency.
|
||||
slices.SortFunc(actionList, func(a, b ActionDetails) int {
|
||||
return strings.Compare(a.Enum, b.Enum)
|
||||
})
|
||||
|
||||
var errorList []error
|
||||
var x int
|
||||
tpl, err := template.New("object.gotmpl").Funcs(template.FuncMap{
|
||||
"capitalize": capitalize,
|
||||
"pascalCaseName": pascalCaseName[string],
|
||||
"actionsList": func() []ActionDetails {
|
||||
return actionList
|
||||
},
|
||||
"actionEnum": func(action policy.Action) string {
|
||||
x++
|
||||
v, ok := actionMap[string(action)]
|
||||
if !ok {
|
||||
errorList = append(errorList, xerrors.Errorf("action value %q does not have a constant a matching enum constant", action))
|
||||
}
|
||||
return v
|
||||
},
|
||||
"concat": func(strs ...string) string { return strings.Join(strs, "") },
|
||||
}).Parse(templateSource)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse template: %w", err)
|
||||
}
|
||||
|
||||
// Convert to sorted list for autogen consistency.
|
||||
var out bytes.Buffer
|
||||
list := make([]Definition, 0)
|
||||
for t, v := range policy.RBACPermissions {
|
||||
v := v
|
||||
list = append(list, Definition{
|
||||
PermissionDefinition: v,
|
||||
Type: t,
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(list, func(a, b Definition) int {
|
||||
return strings.Compare(a.Type, b.Type)
|
||||
})
|
||||
|
||||
err = tpl.Execute(&out, list)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("execute template: %w", err)
|
||||
}
|
||||
|
||||
if len(errorList) > 0 {
|
||||
return nil, errors.Join(errorList...)
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Code generated by typegen/main.go. DO NOT EDIT.
|
||||
package rbac
|
||||
|
||||
import "github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
|
||||
// Objecter returns the RBAC object for itself.
|
||||
type Objecter interface {
|
||||
RBACObject() Object
|
||||
}
|
||||
|
||||
var (
|
||||
{{- range $element := . }}
|
||||
{{- $Name := pascalCaseName $element.FunctionName }}
|
||||
// Resource{{ $Name }}
|
||||
// Valid Actions
|
||||
{{- range $action, $value := .Actions }}
|
||||
// - "{{ actionEnum $action }}" :: {{ $value.Description }}
|
||||
{{- end }}
|
||||
Resource{{ $Name }} = Object {
|
||||
Type: "{{ $element.Type }}",
|
||||
}
|
||||
{{ end -}}
|
||||
)
|
||||
|
||||
func AllResources() []Objecter {
|
||||
return []Objecter{
|
||||
{{- range $element := . }}
|
||||
Resource{{ pascalCaseName $element.FunctionName }},
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
|
||||
func AllActions() []policy.Action {
|
||||
return []policy.Action {
|
||||
{{- range $element := actionsList }}
|
||||
policy.{{ $element.Enum }},
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Code generated by typegen/main.go. DO NOT EDIT.
|
||||
|
||||
import type { RBACAction, RBACResource } from "./typesGenerated";
|
||||
|
||||
// RBACResourceActions maps RBAC resources to their possible actions.
|
||||
// Descriptions are included to document the purpose of each action.
|
||||
// Source is in 'coderd/rbac/policy/policy.go'.
|
||||
export const RBACResourceActions: Partial<
|
||||
Record<RBACResource, Partial<Record<RBACAction, string>>>
|
||||
> = {
|
||||
{{- range $element := . }}
|
||||
{{- if eq $element.Type "*" }}{{ continue }}{{ end }}
|
||||
{{ $element.Type }}: {
|
||||
{{- range $actionValue, $actionDescription := $element.Actions }}
|
||||
{{ $actionValue }}: "{{ $actionDescription }}",
|
||||
{{- end }}
|
||||
},
|
||||
{{- end }}
|
||||
};
|
||||
Reference in New Issue
Block a user