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

feat(apim): crds and controller for configuring apis in apim #1175

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions services/dis-apim-operator/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ go.work
*.swp
*.swo
*~

*.ignore.*
86 changes: 82 additions & 4 deletions services/dis-apim-operator/api/v1alpha1/api_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ limitations under the License.
package v1alpha1

import (
"fmt"

"github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/utils"
apim "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand All @@ -28,18 +32,51 @@ type ApiSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file

// Foo is an example field of Api. Edit api_types.go to remove/update
Foo string `json:"foo,omitempty"`
// DisplayName - The display name of the API. This name is used by the developer portal as the API name.
// +kubebuilder:validation:Required
DisplayName string `json:"displayName,omitempty"`
// Description - Description of the API. May include its purpose, where to get more information, and other relevant information.
// +kubebuilder:validation:Optional
Description *string `json:"description,omitempty"`
// VersioningScheme - Indicates the versioning scheme used for the API. Possible values include, but are not limited to, "Segment", "Query", "Header". Default value is "Segment".
// +kubebuilder:validation:Optional
// +kubebuilder:default:="Segment"
// +kubebuilder:validation:Enum:=Header;Query;Segment
VersioningScheme APIVersionScheme `json:"versioningScheme,omitempty"`
// Path - API prefix. The value is combined with the API version to form the URL of the API endpoint.
// +kubebuilder:validation:Required
Path string `json:"path,omitempty"`
// ApiType - Type of API.
// +kubebuilder:validation:Optional
// +kubebuilder:default:="http"
// +default:value:"http"
// +kubebuilder:validation:Enum:=graphql;http;websocket
ApiType *APIType `json:"apiType,omitempty"`
// Contact - Contact details of the API owner.
// +kubebuilder:validation:Optional
Contact *APIContactInformation `json:"contact,omitempty"`
// Versions - A list of API versions associated with the API. If the API is specified using the OpenAPI definition, then the API version is set by the version field of the OpenAPI definition.
// +kubebuilder:validation:Required
Versions []ApiVersionSubSpec `json:"versions,omitempty"`
Comment on lines +35 to +60
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure required fields are validated appropriately

The new fields added to ApiSpec include proper validation annotations. However, consider the following:

  • Fields marked with +kubebuilder:validation:Required should omit the omitempty tag in the JSON struct tag to enforce validation.

Apply this diff to ensure required fields are correctly validated:

-	DisplayName string `json:"displayName,omitempty"`
+	DisplayName string `json:"displayName"`

Repeat this change for other required fields like Path and Versions.

Committable suggestion skipped: line range outside the PR's diff.

}

// ApiStatus defines the observed state of Api.
type ApiStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
// ProvisioningState - The provisioning state of the API. Possible values are: Provisioning, Succeeded, Failed, Updating, Deleting, and Deleted.
// +kubebuilder:validation:Optional
ProvisioningState ProvisioningState `json:"provisioningState,omitempty"`
// ApiVersionSetID - The identifier of the API Version Set.
// +kubebuilder:validation:Optional
ApiVersionSetID string `json:"apiVersionSetID,omitempty"`
// VersionStates - A list of API Version deployed in the API Management service and the current state of the API Version.
// +kubebuilder:validation:Optional
VersionStates map[string]ApiVersionStatus `json:"versionStates,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.provisioningState`
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"

// Api is the Schema for the apis API.
type Api struct {
Expand All @@ -62,3 +99,44 @@ type ApiList struct {
func init() {
SchemeBuilder.Register(&Api{}, &ApiList{})
}

// GetApiAzureFullName returns the name of the Azure resource.
func (a *Api) GetApiAzureFullName() string {
return fmt.Sprintf("%s-%s", a.Namespace, a.Name)
}
Comment on lines +102 to +106
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Based on the analysis of the codebase, I can now generate the final response:

Azure resource name validation needed for API names

The GetApiAzureFullName method is used to generate Azure resource names for API Management resources, but it lacks validation for Azure's resource naming restrictions. Azure API Management has specific naming constraints:

  • Can only contain alphanumeric characters and hyphens
  • Must be between 1-80 characters long
  • Cannot start or end with a hyphen

Add validation to ensure generated names comply with Azure's requirements:

  • Validate characters in namespace and name
  • Check combined length restrictions
  • Ensure proper hyphen placement
🔗 Analysis chain

Method GetApiAzureFullName could be more robust

Ensure that the namespace and name do not contain characters that could lead to invalid Azure resource names.

Run the following script to check for invalid characters:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the namespace and name contain only valid characters.

# Expect: No output if all names are valid.

# Fetch all Api resources and check for invalid characters
kubectl get apis -A -o json |
  jq -r '.items[] | "\(.metadata.namespace)-\(.metadata.name)"' |
  grep -E '[^a-zA-Z0-9_\-]'

Length of output: 219


Script:

#!/bin/bash
# Let's analyze the codebase to understand the validation and usage of API names

# Check for any validation logic for API names
rg -A 5 "GetApiAzureFullName|validateApiName" --type go

# Look for any Azure resource name validation or sanitization
rg -A 5 "Azure.*Name|resource.*name.*valid" --type go

# Check for any tests related to API naming
fd ".*_test.go" --exec rg -A 5 "GetApiAzureFullName|TestApi.*Name" {}

Length of output: 22440


Script:

#!/bin/bash
# Let's check Azure API documentation or validation logic in the codebase

# Look for Azure API name validation patterns or constants
rg -A 5 "Azure.*name.*pattern|Azure.*name.*validation|Azure.*resource.*name" --type go

# Check for any Azure SDK imports or documentation references
rg -B 2 "azure-sdk-for-go|github.com/Azure" --type go

# Look for any validation functions or error handling related to resource names
ast-grep --pattern 'func $$$validate$$$($$$) $$$'

Length of output: 5655


// ToAzureApiVersionSet returns an APIVersionSetContract object.
func (a *Api) ToAzureApiVersionSet() apim.APIVersionSetContract {
return apim.APIVersionSetContract{
Properties: &apim.APIVersionSetContractProperties{
DisplayName: &a.Spec.DisplayName,
VersioningScheme: a.Spec.VersioningScheme.AzureAPIVersionScheme(),
Description: a.Spec.Description,
},
Name: utils.ToPointer(a.GetApiAzureFullName()),
}
}
Comment on lines +103 to +118
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle possible nil values in methods

In the methods ToAzureApiVersionSet and GetApiAzureFullName, ensure that all accessed fields are properly checked for nil to prevent runtime panics.

Review and update the methods to include nil checks where necessary.


// ToApiVersions returns a map of ApiVersion type.
func (a *Api) ToApiVersions() map[string]ApiVersion {
apiVersions := make(map[string]ApiVersion)
for _, version := range a.Spec.Versions {
versionFullName := version.GetApiVersionFullName(a.Name)
apiVersion := ApiVersion{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: versionFullName,
Namespace: a.Namespace,
},
Spec: ApiVersionSpec{
ApiVersionSetId: a.Status.ApiVersionSetID,
ApiVersionScheme: a.Spec.VersioningScheme,
Path: a.Spec.Path,
ApiType: a.Spec.ApiType,
ApiVersionSubSpec: version,
},
}
apiVersions[version.GetApiVersionSpecifier()] = apiVersion
}
return apiVersions
}
157 changes: 157 additions & 0 deletions services/dis-apim-operator/api/v1alpha1/apiversion_enums.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package v1alpha1

import (
"github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/utils"
apim "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2"
)

// INSERT ADDITIONAL TYPES
// Important: Run "make" to regenerate code after modifying this file

// ContentFormat - Format of the Content in which the API is getting imported.
type ContentFormat string

const (
// ContentFormatGraphqlLink - The GraphQL API endpoint hosted on a publicly accessible internet address.
ContentFormatGraphqlLink ContentFormat = "graphql-link"
// ContentFormatOpenapi - The contents are inline and Content Type is a OpenAPI 3.0 YAML Document.
ContentFormatOpenapi ContentFormat = "openapi"
// ContentFormatOpenapiJSON - The contents are inline and Content Type is a OpenAPI 3.0 JSON Document.
ContentFormatOpenapiJSON ContentFormat = "openapi+json"
// ContentFormatOpenapiJSONLink - The OpenAPI 3.0 JSON document is hosted on a publicly accessible internet address.
ContentFormatOpenapiJSONLink ContentFormat = "openapi+json-link"
// ContentFormatOpenapiLink - The OpenAPI 3.0 YAML document is hosted on a publicly accessible internet address.
ContentFormatOpenapiLink ContentFormat = "openapi-link"
// ContentFormatSwaggerJSON - The contents are inline and Content Type is a OpenAPI 2.0 JSON Document.
ContentFormatSwaggerJSON ContentFormat = "swagger-json"
// ContentFormatSwaggerLinkJSON - The OpenAPI 2.0 JSON document is hosted on a publicly accessible internet address.
ContentFormatSwaggerLinkJSON ContentFormat = "swagger-link-json"
// ContentFormatWadlLinkJSON - The WADL document is hosted on a publicly accessible internet address.
ContentFormatWadlLinkJSON ContentFormat = "wadl-link-json"
// ContentFormatWadlXML - The contents are inline and Content type is a WADL document.
ContentFormatWadlXML ContentFormat = "wadl-xml"
)

func (c ContentFormat) AzureContentFormat() *apim.ContentFormat {
contentFormat := apim.ContentFormat(c)
return &contentFormat
}

type APIContactInformation struct {
// The email address of the contact person/organization. MUST be in the format of an email address
Email *string `json:"email,omitempty"`

// The identifying name of the contact person/organization
Name *string `json:"name,omitempty"`

// The URL pointing to the contact information. MUST be in the format of a URL
URL *string `json:"url,omitempty"`
}

func (a *APIContactInformation) AzureAPIContactInformation() *apim.APIContactInformation {
if a == nil {
return nil
}
return &apim.APIContactInformation{
Email: a.Email,
Name: a.Name,
URL: a.URL,
}
}

type APIVersionScheme string

const (
// APIVersionSetContractDetailsVersioningSchemeHeader - The API Version is passed in a HTTP header.
APIVersionSetContractDetailsVersioningSchemeHeader APIVersionScheme = "Header"
// APIVersionSetContractDetailsVersioningSchemeQuery - The API Version is passed in a query parameter.
APIVersionSetContractDetailsVersioningSchemeQuery APIVersionScheme = "Query"
// APIVersionSetContractDetailsVersioningSchemeSegment - The API Version is passed in a path segment.
APIVersionSetContractDetailsVersioningSchemeSegment APIVersionScheme = "Segment"
)

func (a *APIVersionScheme) AzureAPIVersionScheme() *apim.VersioningScheme {
if a == nil {
return nil
}
apiVersionScheme := apim.VersioningScheme(*a)
return &apiVersionScheme
}

func (a *APIVersionScheme) AzureAPIVersionSetContractDetailsVersioningScheme() *apim.APIVersionSetContractDetailsVersioningScheme {
if a == nil {
return nil
}
apiVersionScheme := apim.APIVersionSetContractDetailsVersioningScheme(*a)
return &apiVersionScheme
}

type Protocol string

const (
ProtocolHTTP Protocol = "http"
ProtocolHTTPS Protocol = "https"
ProtocolWs Protocol = "ws"
ProtocolWss Protocol = "wss"
)

func (p *Protocol) AzureProtocol() *apim.Protocol {
if p == nil {
return nil
}
protocol := apim.Protocol(*p)
return &protocol
}

func ToApimProtocolSlice(protocols []Protocol) []*apim.Protocol {
apimProtocols := make([]*apim.Protocol, len(protocols))
for i, protocol := range protocols {
apimProtocols[i] = utils.ToPointer(apim.Protocol(protocol))
}
return apimProtocols
}

type PolicyFormat string

const (
// PolicyContentFormatRawxml - The contents are inline and Content type is a non XML encoded policy document.
PolicyContentFormatRawxml PolicyFormat = "rawxml"
// PolicyContentFormatRawxmlLink - The policy document is not XML encoded and is hosted on a HTTP endpoint accessible from
// the API Management service.
PolicyContentFormatRawxmlLink PolicyFormat = "rawxml-link"
// PolicyContentFormatXML - The contents are inline and Content type is an XML document.
PolicyContentFormatXML PolicyFormat = "xml"
// PolicyContentFormatXMLLink - The policy XML document is hosted on a HTTP endpoint accessible from the API Management service.
PolicyContentFormatXMLLink PolicyFormat = "xml-link"
)

func (p *PolicyFormat) AzurePolicyFormat() *apim.PolicyContentFormat {
if p == nil {
return nil
}
policyFormat := apim.PolicyContentFormat(*p)
return &policyFormat
}

// APIType - Type of API.
type APIType string

const (
APITypeGraphql APIType = "graphql"
APITypeHTTP APIType = "http"
APITypeWebsocket APIType = "websocket"
)

func (a APIType) AzureApiType() *apim.APIType {
apiType := apim.APIType(a)
return &apiType
}

type ProvisioningState string

const (
ProvisioningStateSucceeded ProvisioningState = "Succeeded"
ProvisioningStateFailed ProvisioningState = "Failed"
ProvisioningStateUpdating ProvisioningState = "Updating"
ProvisioningStateDeleting ProvisioningState = "Deleting"
)
Loading
Loading