Skip to content
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

add admin component #569

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions admin/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Base and builder image will need to be replaced by Fips compliant one
FROM --platform=${TARGETPLATFORM:-linux/amd64} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-cbl-mariner2.0@sha256:04ef70d7877189fcaafb649a442c43f9666a5ecf5b05c40ca0bfeddeec531efb as builder

WORKDIR /app
ADD archive.tar.gz .
# https://github.com/microsoft/go/tree/microsoft/main/eng/doc/fips#build-option-to-require-fips-mode
ENV CGO_ENABLED=1 GOFLAGS='-tags=requirefips'
RUN cd admin && make admin

FROM --platform=${TARGETPLATFORM:-linux/amd64} mcr.microsoft.com/cbl-mariner/distroless/base:2.0-nonroot@sha256:748adcb84f1132a27fd31b5cbaeaf2ff373d795babf35107dc6af05fab196ffc
WORKDIR /
COPY --from=builder /app/admin/aro-hcp-admin .
ENTRYPOINT ["/aro-hcp-admin"]
55 changes: 55 additions & 0 deletions admin/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module github.com/Azure/ARO-HCP/admin

go 1.22.2

require (
github.com/Azure/ARO-HCP/internal v0.0.0-20240905143746-fba3667d52ff
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
github.com/openshift-online/ocm-sdk-go v0.1.438
github.com/openshift/api v0.0.0-20240429104249-ac9356ba1784
)

require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/glog v1.2.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.20.2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.30.0 // indirect
k8s.io/apimachinery v0.30.0 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)
192 changes: 192 additions & 0 deletions admin/go.sum

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions admin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"context"
"log/slog"
"net"
"os"
"time"

"github.com/Azure/ARO-HCP/admin/pkg/admin"
)

func main() {

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

listener, _ := net.Listen("tcp", ":8446")

// Initialize the Admin server
admin := admin.NewAdmin(logger, listener, "us-east")

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
time.Sleep(10 * time.Second)
cancel()
}()

admin.Run(ctx, nil)
}
63 changes: 63 additions & 0 deletions admin/pkg/admin/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package admin

import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"strings"
"sync/atomic"

"github.com/Azure/ARO-HCP/internal/ocm"
)

type Admin struct {
clusterServiceConfig ocm.ClusterServiceConfig
server http.Server
listener net.Listener
logger *slog.Logger
location string
done chan struct{}
ready atomic.Bool
}

func NewAdmin(logger *slog.Logger, listener net.Listener, location string) *Admin {
a := &Admin{
logger: logger,
listener: listener,
location: strings.ToLower(location),
done: make(chan struct{}),
}

// Set up http.Server and routes via the separate routes() function
a.server = http.Server{
Handler: a.adminRoutes(), // Separate function for setting up ServeMux
BaseContext: func(net.Listener) context.Context {
return context.WithValue(context.Background(), "logger", logger)
},
}

return a
}

func (a *Admin) Run(ctx context.Context, stop <-chan struct{}) {
if stop != nil {
go func() {
<-stop
a.ready.Store(false)
_ = a.server.Shutdown(ctx)
}()
}

a.logger.Info(fmt.Sprintf("listening on %s", a.listener.Addr().String()))
a.ready.Store(true)

if err := a.server.Serve(a.listener); err != nil && err != http.ErrServerClosed {
a.logger.Error(err.Error())
os.Exit(1)
}

close(a.done)
}
156 changes: 156 additions & 0 deletions admin/pkg/admin/adminResourceList.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you to decide (because the goal can totally change), but let's say that the goal of this Geneva Action is to get a list of all clusters from OCM. This will probably look something like:

GET /v1/ocm/clusters

With room for a future Geneva Action to provide cluster_id or cluster_name, like

GET /v1/ocm/clusters/id/${ID}
GET /v1/ocm/clusters?name=mycluster

I think this workload's "API" will be radically different from the RP/frontend

Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package admin

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"

"github.com/Azure/ARO-HCP/internal/api"
"github.com/Azure/ARO-HCP/internal/api/arm"
azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
)

func (a *Admin) AdminResourceList(writer http.ResponseWriter, request *http.Request) {
ctx := request.Context()

versionedInterface, err := VersionFromContext(ctx)
if err != nil {
a.logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}

var query string
subscriptionId := request.PathValue(PathSegmentSubscriptionID)
resourceGroupName := request.PathValue(PathSegmentResourceGroupName)
location := request.PathValue(PathSegmentLocation)

switch {
case resourceGroupName != "":
query = fmt.Sprintf("azure.resource_group_name='%s'", resourceGroupName)
case location != "":
query = fmt.Sprintf("region.id='%s'", location)
case subscriptionId != "" && location == "" && resourceGroupName == "":
query = fmt.Sprintf("azure.subscription_id='%s'", subscriptionId)
}

pageSize := 10
pageNumber := 1

if pageStr := request.URL.Query().Get("page"); pageStr != "" {
pageNumber, _ = strconv.Atoi(pageStr)
}
if sizeStr := request.URL.Query().Get("size"); sizeStr != "" {
pageSize, _ = strconv.Atoi(sizeStr)
}

// Create the request with initial parameters:
clustersRequest := a.clusterServiceConfig.Conn.ClustersMgmt().V2alpha1().Clusters().List().Search(query)
clustersRequest.Size(pageSize)
clustersRequest.Page(pageNumber)

// Send the initial request:
clustersListResponse, err := clustersRequest.SendContext(ctx)
if err != nil {
a.logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}

var hcpCluster *api.HCPOpenShiftCluster
var versionedHcpClusters []*api.VersionedHCPOpenShiftCluster
clusters := clustersListResponse.Items().Slice()
for _, cluster := range clusters {
// FIXME Temporary, until we have a real ResourceID to pass.
azcoreResourceID, err := azcorearm.ParseResourceID(fmt.Sprintf(
"/subscriptions/%s/resourceGroups/%s/providers/%s/%s",
subscriptionId, resourceGroupName, api.ResourceType,
cluster.Azure().ResourceName()))
if err != nil {
a.logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
resourceID := &arm.ResourceID{ResourceID: *azcoreResourceID}
hcpCluster = ConvertCStoHCPOpenShiftCluster(resourceID, cluster)
versionedResource := versionedInterface.NewHCPOpenShiftCluster(hcpCluster)
versionedHcpClusters = append(versionedHcpClusters, &versionedResource)
}

// Check if there are more pages to fetch and set NextLink if applicable:
var nextLink string
if clustersListResponse.Size() >= pageSize {
nextPage := pageNumber + 1
nextLink = buildNextLink(request.URL.Path, request.URL.Query(), nextPage, pageSize)
}

result := api.VersionedHCPOpenShiftClusterList{
Value: versionedHcpClusters,
NextLink: &nextLink,
}

resp, err := json.Marshal(result)
if err != nil {
a.logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}

_, err = writer.Write(resp)
if err != nil {
a.logger.Error(err.Error())
}
}

func VersionFromContext(ctx context.Context) (api.Version, error) {
version, ok := ctx.Value(contextKeyVersion).(api.Version)
if !ok {
err := &ContextError{
got: version,
}
return version, err
}
return version, nil
}

func (c *ContextError) Error() string {
return fmt.Sprintf(
"error retrieving value from context, value obtained was '%v' and type obtained was '%T'",
c.got,
c.got)
}

type contextKey int
type ContextError struct {
got any
}

const (
// Keys for request-scoped data in http.Request contexts
contextKeyOriginalPath contextKey = iota
contextKeyBody
contextKeyLogger
contextKeyVersion
contextKeyResourceID
contextKeyCorrelationData
contextKeySystemData
contextKeySubscription
)

func buildNextLink(basePath string, queryParams url.Values, nextPage, pageSize int) string {
// Clone the existing query parameters
newParams := make(url.Values)
for key, values := range queryParams {
newParams[key] = values
}

newParams.Set("page", strconv.Itoa(nextPage))
newParams.Set("size", strconv.Itoa(pageSize))

// Construct the next link URL
nextLink := basePath + "?" + newParams.Encode()
return nextLink
}
17 changes: 17 additions & 0 deletions admin/pkg/admin/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package admin

const (
ProgramName = "ARO HCP Admin"

// APIVersionKey is the request parameter name for the API version.
APIVersionKey = "api-version"

// Wildcard path segment names for request multiplexing, must be lowercase as we lowercase the request URL pattern when registering handlers
PathSegmentActionName = "actionname"
PathSegmentDeploymentName = "deploymentname"
PathSegmentLocation = "location"
PathSegmentNodePoolName = "nodepoolname"
PathSegmentResourceGroupName = "resourcegroupname"
PathSegmentResourceName = "resourcename"
PathSegmentSubscriptionID = "subscriptionid"
)
Loading
Loading