Skip to content
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
1 change: 1 addition & 0 deletions admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This prefix is abbreviated as `{resourceId}` below.
|--------|------|-------------|
| `PUT` | `/admin/v1/hcp{resourceId}/breakglass?group=...&ttl=...` | Create a breakglass session ([details](breakglass.md)) |
| `GET` | `/admin/v1/hcp{resourceId}/breakglass/{sessionName}/kubeconfig` | Get kubeconfig for a breakglass session ([details](breakglass.md)) |
| `GET` | `/admin/v1/hcp{resourceId}/serialconsole?vmName=...` | Retrieve serial console logs for a VM |
| `GET` | `/admin/v1/hcp{resourceId}/cosmosdump` | Cosmos DB dump for a cluster |
| `GET` | `/admin/v1/hcp{resourceId}/helloworld` | HCP hello world (dev/test) |
| `GET` | `/admin/helloworld` | Hello world (dev/test) |
Expand Down
1 change: 1 addition & 0 deletions admin/server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/Azure/ARO-HCP/sessiongate v0.0.0-00010101000000-000000000000
github.com/Azure/azure-kusto-go v0.16.1
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0
github.com/go-logr/logr v1.4.3
github.com/microsoft/go-otel-audit v0.2.2
Expand Down
6 changes: 6 additions & 0 deletions admin/server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1 h1:ToPLhnXvatKVN4Zkcx
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1/go.mod h1:Krtog/7tz27z75TwM5cIS8bxEH4dcBUezcq+kGVeZEo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.1 h1:1kpY4qe+BGAH2ykv4baVSqyx+AY5VjXeJ15SldlU6hs=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.1/go.mod h1:nT6cWpWdUt+g81yuKmjeYPUtI73Ak3yQIT4PVVsCEEQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0 h1:HYGD75g0bQ3VO/Omedm54v4LrD3B1cGImuRF3AJ5wLo=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0/go.mod h1:ulHyBFJOI0ONiRL4vcJTmS7rx18jQQlEPmAgo80cRdM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeployments v0.2.0 h1:bYq3jfB2x36hslKMHyge3+esWzROtJNk/4dCjsKlrl4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeployments v0.2.0/go.mod h1:fewgRjNVE84QVVh798sIMFb7gPXPp7NmnekGnboSnXk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v3 v3.0.1 h1:guyQA4b8XB2sbJZXzUnOF9mn0WDBv/ZT7me9wTipKtE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v3 v3.0.1/go.mod h1:8h8yhzh9o+0HeSIhUxYny+rEQajScrfIpNktvgYG3Q8=
github.com/Azure/retry v0.0.0-20250221010952-92c9290cea0f h1:XjKfallhRhddiRmBG0u2gs+Rd75QjvAlPVDy3ZWLjPg=
Expand Down
21 changes: 3 additions & 18 deletions admin/server/handlers/hcp/breakglass/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package breakglass

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
Expand All @@ -25,8 +24,7 @@ import (
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/utils/set"

ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"

hcphelpers "github.com/Azure/ARO-HCP/admin/server/handlers/hcp"
"github.com/Azure/ARO-HCP/admin/server/middleware"
"github.com/Azure/ARO-HCP/internal/api/arm"
"github.com/Azure/ARO-HCP/internal/database"
Expand Down Expand Up @@ -73,12 +71,12 @@ func (h *HCPBreakglassSessionCreationHandler) ServeHTTP(writer http.ResponseWrit

clusterHypershiftDetails, err := h.csClient.GetClusterHypershiftDetails(request.Context(), hcp.ServiceProviderProperties.ClusterServiceID)
if err != nil {
return clusterServiceError(err, "hypershift details")
return hcphelpers.ClusterServiceError(err, "hypershift details")
}

provisionShard, err := h.csClient.GetClusterProvisionShard(request.Context(), hcp.ServiceProviderProperties.ClusterServiceID)
if err != nil {
return clusterServiceError(err, "provision shard")
return hcphelpers.ClusterServiceError(err, "provision shard")
}

group, ttl, err := h.validateSessionParameters(request)
Expand Down Expand Up @@ -185,16 +183,3 @@ func (h *HCPBreakglassSessionCreationHandler) validateSessionParameters(request

return body.Group, ttl, utilerrors.NewAggregate(errs)
}

// clusterServiceError checks if err is an OCM not-found error and returns a
// specific CloudError. This prevents ReportError from misinterpreting it as
// "HCP resource not found" (the HCP was already found in the database).
// Non-OCM errors are wrapped for ReportError to handle as internal errors.
func clusterServiceError(err error, what string) error {
var ocmErr *ocmerrors.Error
if errors.As(err, &ocmErr) && ocmErr.Status() == http.StatusNotFound {
return arm.NewCloudError(http.StatusNotFound, arm.CloudErrorCodeNotFound, "",
"%s not found in cluster service", what)
}
return fmt.Errorf("failed to get %s from cluster service: %w", what, err)
}
38 changes: 38 additions & 0 deletions admin/server/handlers/hcp/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2025 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hcp

import (
"errors"
"fmt"
"net/http"

ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"

"github.com/Azure/ARO-HCP/internal/api/arm"
)

// ClusterServiceError checks if err is an OCM not-found error and returns a
// specific CloudError. This prevents ReportError from misinterpreting it as
// "HCP resource not found" (the HCP was already found in the database).
// Non-OCM errors are wrapped for ReportError to handle as internal errors.
func ClusterServiceError(err error, what string) error {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Initially in the PR this was being used by the serialconsolelogs implementation, but since it no longer uses it, this can be moved back to breakglass. But decided to keep it here for any similar future use.
Please comment if anyone feel against moving the function here in this PR.

var ocmErr *ocmerrors.Error
if errors.As(err, &ocmErr) && ocmErr.Status() == http.StatusNotFound {
return arm.NewCloudError(http.StatusNotFound, arm.CloudErrorCodeNotFound, "",
"%s not found in cluster service", what)
}
return fmt.Errorf("failed to get %s from cluster service: %w", what, err)
}
147 changes: 147 additions & 0 deletions admin/server/handlers/hcp/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2025 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hcp

import (
"errors"
"net/http"
"strings"
"testing"

ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"

"github.com/Azure/ARO-HCP/internal/api/arm"
)

func TestClusterServiceError(t *testing.T) {
// Helper to create OCM errors
createOCMError := func(status int) error {
ocmErr, err := ocmerrors.NewError().Status(status).Build()
if err != nil {
t.Fatalf("Failed to create OCM error: %v", err)
}
return ocmErr
}

tests := []struct {
name string
err error
what string
expectedStatusCode int
expectedErrorCode string
expectedMessage string
isCloudError bool
}{
{
name: "OCM not-found error returns 404 CloudError",
err: createOCMError(http.StatusNotFound),
what: "cluster data",
expectedStatusCode: http.StatusNotFound,
expectedErrorCode: arm.CloudErrorCodeNotFound,
expectedMessage: "cluster data not found in cluster service",
isCloudError: true,
},
{
name: "OCM not-found error for hypershift details",
err: createOCMError(http.StatusNotFound),
what: "hypershift details",
expectedStatusCode: http.StatusNotFound,
expectedErrorCode: arm.CloudErrorCodeNotFound,
expectedMessage: "hypershift details not found in cluster service",
isCloudError: true,
},
{
name: "OCM internal server error wraps error",
err: createOCMError(http.StatusInternalServerError),
what: "cluster data",
expectedMessage: "failed to get cluster data from cluster service",
isCloudError: false,
},
{
name: "OCM bad request error wraps error",
err: createOCMError(http.StatusBadRequest),
what: "provision shard",
expectedMessage: "failed to get provision shard from cluster service",
isCloudError: false,
},
{
name: "OCM unauthorized error wraps error",
err: createOCMError(http.StatusUnauthorized),
what: "cluster data",
expectedMessage: "failed to get cluster data from cluster service",
isCloudError: false,
},
{
name: "non-OCM error wraps error",
err: errors.New("network timeout"),
what: "cluster data",
expectedMessage: "failed to get cluster data from cluster service",
isCloudError: false,
},
{
name: "generic error wraps error",
err: errors.New("connection refused"),
what: "hypershift details",
expectedMessage: "failed to get hypershift details from cluster service",
isCloudError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ClusterServiceError(tt.err, tt.what)

if result == nil {
t.Fatal("Expected error but got nil")
}

if tt.isCloudError {
// Check if it's a CloudError
var cloudErr *arm.CloudError
if !errors.As(result, &cloudErr) {
t.Fatalf("Expected CloudError but got %T: %v", result, result)
}

if cloudErr.StatusCode != tt.expectedStatusCode {
t.Errorf("Expected status code %d, got %d", tt.expectedStatusCode, cloudErr.StatusCode)
}

if cloudErr.Code != tt.expectedErrorCode {
t.Errorf("Expected error code %q, got %q", tt.expectedErrorCode, cloudErr.Code)
}

if cloudErr.Message != tt.expectedMessage {
t.Errorf("Expected message %q, got %q", tt.expectedMessage, cloudErr.Message)
}
} else {
// Check if it's a wrapped error (not CloudError)
var cloudErr *arm.CloudError
if errors.As(result, &cloudErr) {
t.Fatalf("Expected wrapped error but got CloudError: %v", result)
}

// Verify error message contains expected text
if !strings.Contains(result.Error(), tt.expectedMessage) {
t.Errorf("Expected error to contain %q, got %q", tt.expectedMessage, result.Error())
}

// Verify original error is wrapped
if !errors.Is(result, tt.err) {
t.Errorf("Expected error to wrap original error")
}
}
})
}
}
Loading
Loading