diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 45642a6f..ab89020d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.21' - run: make fmt vet: @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.21' - run: make vet build: @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.21' - run: make cmd test-unit: @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.21' - run: make test test-api: @@ -60,7 +60,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.21' - run: | rm -f $DB_PATH make run & diff --git a/.github/workflows/test-nightly.yml b/.github/workflows/test-nightly.yml index 60e81cf8..90350c81 100644 --- a/.github/workflows/test-nightly.yml +++ b/.github/workflows/test-nightly.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.21' - run: make test test-api: @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.21' - run: | make vet DISCONNECTED=1 make run & diff --git a/Makefile b/Makefile index e8463bc8..b092ad5f 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,7 @@ $(CONTROLLERGEN): # Ensure goimports installed. $(GOIMPORTS): - go install golang.org/x/tools/cmd/goimports@latest + go install golang.org/x/tools/cmd/goimports@v0.24 # Build SAMPLE ADDON addon: fmt vet diff --git a/api/error.go b/api/error.go index 01dc4a4b..fe79e47d 100644 --- a/api/error.go +++ b/api/error.go @@ -2,6 +2,7 @@ package api import ( "errors" + "fmt" "net/http" "os" @@ -78,6 +79,22 @@ func (r *Forbidden) Is(err error) (matched bool) { return } +// NotFound reports resource not-found errors. +type NotFound struct { + Resource string + Reason string +} + +func (r *NotFound) Error() string { + return fmt.Sprintf("Resource '%s' not found. %s", r.Resource, r.Reason) +} + +func (r *NotFound) Is(err error) (matched bool) { + var forbidden *Forbidden + matched = errors.As(err, &forbidden) + return +} + // ErrorHandler handles error conditions from lower handlers. func ErrorHandler() gin.HandlerFunc { return func(ctx *gin.Context) { @@ -102,7 +119,8 @@ func ErrorHandler() gin.HandlerFunc { return } - if errors.Is(err, gorm.ErrRecordNotFound) { + if errors.Is(err, gorm.ErrRecordNotFound) || + errors.Is(err, &NotFound{}) { if ctx.Request.Method == http.MethodDelete { rtx.Status(http.StatusNoContent) return diff --git a/api/pkg.go b/api/pkg.go index 36f78c05..18efbcb3 100644 --- a/api/pkg.go +++ b/api/pkg.go @@ -78,6 +78,7 @@ func All() []Handler { &RuleSetHandler{}, &SchemaHandler{}, &SettingHandler{}, + &ServiceHandler{}, &StakeholderHandler{}, &StakeholderGroupHandler{}, &TagHandler{}, diff --git a/api/service.go b/api/service.go new file mode 100644 index 00000000..d3e26972 --- /dev/null +++ b/api/service.go @@ -0,0 +1,99 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "os" + + "github.com/gin-gonic/gin" +) + +// Routes +const ( + ServicesRoot = "/services" + ServiceRoot = ServicesRoot + "/:name/*" + Wildcard +) + +// serviceRoutes name to route map. +var serviceRoutes = map[string]string{ + "kai": os.Getenv("KAI_URL"), +} + +// ServiceHandler handles service routes. +type ServiceHandler struct { + BaseHandler +} + +// AddRoutes adds routes. +func (h ServiceHandler) AddRoutes(e *gin.Engine) { + e.GET(ServicesRoot, h.List) + e.Any(ServiceRoot, h.Required, h.Forward) +} + +// List godoc +// @summary List named service routes. +// @description List named service routes. +// @tags services +// @produce json +// @success 200 {object} api.Service +// @router /services [get] +func (h ServiceHandler) List(ctx *gin.Context) { + var r []Service + for name, route := range serviceRoutes { + service := Service{Name: name, Route: route} + r = append(r, service) + } + + h.Respond(ctx, http.StatusOK, r) +} + +// Required enforces RBAC. +func (h ServiceHandler) Required(ctx *gin.Context) { + Required(ctx.Param(Name))(ctx) +} + +// Forward provides RBAC and forwards request to the service. +func (h ServiceHandler) Forward(ctx *gin.Context) { + path := ctx.Param(Wildcard) + name := ctx.Param(Name) + route, found := serviceRoutes[name] + if !found { + err := &NotFound{Resource: name} + _ = ctx.Error(err) + return + } + if route == "" { + err := fmt.Errorf("route for: '%s' not defined", name) + _ = ctx.Error(err) + return + } + u, err := url.Parse(route) + if err != nil { + err = &BadRequestError{Reason: err.Error()} + _ = ctx.Error(err) + return + } + proxy := httputil.ReverseProxy{ + Director: func(req *http.Request) { + req.URL.Scheme = u.Scheme + req.URL.Host = u.Host + req.URL.Path = path + Log.Info( + "Routing (service)", + "path", + ctx.Request.URL.Path, + "route", + req.URL.String()) + }, + } + + proxy.ServeHTTP(ctx.Writer, ctx.Request) +} + +// Service REST resource. +type Service struct { + Name string `json:"name"` + Route string `json:"route"` +} diff --git a/auth/roles.yaml b/auth/roles.yaml index aa65e7a7..361a2c1a 100644 --- a/auth/roles.yaml +++ b/auth/roles.yaml @@ -82,6 +82,10 @@ - get - post - put + - name: kai + verbs: + - get + - post - name: proxies verbs: - delete @@ -286,6 +290,10 @@ - get - post - put + - name: kai + verbs: + - get + - post - name: proxies verbs: - get @@ -443,6 +451,10 @@ - name: jobfunctions verbs: - get + - name: kai + verbs: + - get + - post - name: proxies verbs: - get @@ -560,6 +572,10 @@ - name: jobfunctions verbs: - get + - name: kai + verbs: + - get + - post - name: proxies verbs: - get