Skip to content

Commit

Permalink
3rd step toward Investigate and propose package repositories API with…
Browse files Browse the repository at this point in the history
… similar core interface to packages API. vmware-tanzu#3496  (vmware-tanzu#4514)

* attempt #2

* 1st checkin

* 2nd checkpoint

* attempt #2

* small fixes

* small fix
  • Loading branch information
gfichtenholt authored Mar 29, 2022
1 parent c6b7763 commit b0b8dfc
Show file tree
Hide file tree
Showing 25 changed files with 2,039 additions and 217 deletions.
39 changes: 39 additions & 0 deletions cmd/kubeapps-apis/core/packages/v1alpha1/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"
"fmt"

. "github.com/ahmetb/go-linq/v3"

pluginsv1alpha1 "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/core/plugins/v1alpha1"
packages "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/packages/v1alpha1"
"github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/plugins/v1alpha1"
Expand Down Expand Up @@ -110,6 +112,43 @@ func (s repositoriesServer) GetPackageRepositoryDetail(ctx context.Context, requ
}, nil
}

// GetPackageRepositorySummaries returns the package repository summaries based on the request.
func (s repositoriesServer) GetPackageRepositorySummaries(ctx context.Context, request *packages.GetPackageRepositorySummariesRequest) (*packages.GetPackageRepositorySummariesResponse, error) {
contextMsg := fmt.Sprintf("(cluster=%q, namespace=%q)", request.GetContext().GetCluster(), request.GetContext().GetNamespace())
log.Infof("+core GetPackageRepositorySummaries %s", contextMsg)

// Aggregate the response for each plugin
summaries := []*packages.PackageRepositorySummary{}
// TODO: We can do these in parallel in separate go routines.
for _, p := range s.pluginsWithServers {
response, err := p.server.GetPackageRepositorySummaries(ctx, request)
if err != nil {
return nil, status.Errorf(status.Convert(err).Code(), "Invalid GetPackageRepositorySummaries response from the plugin %v: %v", p.plugin.Name, err)
}

// Add the plugin for the pkgs
pluginSummaries := response.PackageRepositorySummaries
for _, r := range pluginSummaries {
if r.PackageRepoRef == nil {
r.PackageRepoRef = &packages.PackageRepositoryReference{}
}
r.PackageRepoRef.Plugin = p.plugin
}
summaries = append(summaries, pluginSummaries...)
}

From(summaries).
// Order by repo name, regardless of the plugin
OrderBy(func(repo interface{}) interface{} {
return repo.(*packages.PackageRepositorySummary).Name + repo.(*packages.PackageRepositorySummary).PackageRepoRef.Plugin.Name
}).ToSlice(&summaries)

// Build the response
return &packages.GetPackageRepositorySummariesResponse{
PackageRepositorySummaries: summaries,
}, nil
}

// getPluginWithServer returns the *pkgPluginsWithServer from a given packagesServer
// matching the plugin name
func (s repositoriesServer) getPluginWithServer(plugin *v1alpha1.Plugin) *repoPluginsWithServer {
Expand Down
75 changes: 73 additions & 2 deletions cmd/kubeapps-apis/core/packages/v1alpha1/repositories_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ var mockedRepoPlugin2 = makeDefaultTestRepositoriesPlugin("mock2")
var mockedNotFoundRepoPlugin = makeOnlyStatusTestRepositoriesPlugin("bad-plugin", codes.NotFound)

var ignoreUnexportedRepoOpts = cmpopts.IgnoreUnexported(
corev1.AddPackageRepositoryRequest{},
corev1.AddPackageRepositoryResponse{},
corev1.Context{},
plugins.Plugin{},
corev1.PackageRepositoryReference{},
corev1.GetPackageRepositoryDetailRequest{},
corev1.GetPackageRepositoryDetailResponse{},
corev1.PackageRepositoryDetail{},
corev1.PackageRepositoryStatus{},
corev1.GetPackageRepositorySummariesResponse{},
corev1.PackageRepositorySummary{},
)

func makeDefaultTestRepositoriesPlugin(pluginName string) repoPluginsWithServer {
Expand All @@ -40,6 +40,11 @@ func makeDefaultTestRepositoriesPlugin(pluginName string) repoPluginsWithServer
repositoriesPluginServer.PackageRepositoryDetail =
plugin_test.MakePackageRepositoryDetail("repo-1", pluginDetails)

repositoriesPluginServer.PackageRepositorySummaries = []*corev1.PackageRepositorySummary{
plugin_test.MakePackageRepositorySummary("repo-2", pluginDetails),
plugin_test.MakePackageRepositorySummary("repo-1", pluginDetails),
}

return repoPluginsWithServer{
plugin: pluginDetails,
server: repositoriesPluginServer,
Expand Down Expand Up @@ -215,3 +220,69 @@ func TestGetPackageRepositoryDetail(t *testing.T) {
})
}
}

func TestGetPackageRepositorySummaries(t *testing.T) {
testCases := []struct {
name string
configuredPlugins []repoPluginsWithServer
statusCode codes.Code
request *corev1.GetPackageRepositorySummariesRequest
expectedResponse *corev1.GetPackageRepositorySummariesResponse
}{
{
name: "it should successfully call the core GetPackageRepositorySummaries operation",
configuredPlugins: []repoPluginsWithServer{
mockedRepoPlugin1,
mockedRepoPlugin2,
},
request: &corev1.GetPackageRepositorySummariesRequest{
Context: &corev1.Context{
Cluster: plugin_test.GlobalPackagingCluster,
Namespace: plugin_test.DefaultNamespace,
},
},
expectedResponse: &corev1.GetPackageRepositorySummariesResponse{
PackageRepositorySummaries: []*corev1.PackageRepositorySummary{
plugin_test.MakePackageRepositorySummary("repo-1", mockedPackagingPlugin1.plugin),
plugin_test.MakePackageRepositorySummary("repo-1", mockedPackagingPlugin2.plugin),
plugin_test.MakePackageRepositorySummary("repo-2", mockedPackagingPlugin1.plugin),
plugin_test.MakePackageRepositorySummary("repo-2", mockedPackagingPlugin2.plugin),
},
},
statusCode: codes.OK,
},
{
name: "it should fail when calling the core GetPackageRepositorySummaries operation when the package is not present in a plugin",
configuredPlugins: []repoPluginsWithServer{
mockedRepoPlugin1,
mockedNotFoundRepoPlugin,
},
request: &corev1.GetPackageRepositorySummariesRequest{
Context: &corev1.Context{
Cluster: plugin_test.GlobalPackagingCluster,
Namespace: plugin_test.DefaultNamespace,
},
},
statusCode: codes.NotFound,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
server := &repositoriesServer{
pluginsWithServers: tc.configuredPlugins,
}
repoSummaries, err := server.GetPackageRepositorySummaries(context.Background(), tc.request)

if got, want := status.Code(err), tc.statusCode; got != want {
t.Fatalf("got: %+v, want: %+v, err: %+v", got, want, err)
}

if tc.statusCode == codes.OK {
if got, want := repoSummaries, tc.expectedResponse; !cmp.Equal(got, want, ignoreUnexportedRepoOpts) {
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, ignoreUnexportedRepoOpts))
}
}
})
}
}
128 changes: 128 additions & 0 deletions cmd/kubeapps-apis/docs/kubeapps-apis.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,46 @@
}
},
"/core/packages/v1alpha1/repositories": {
"get": {
"operationId": "RepositoriesService_GetPackageRepositorySummaries",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1alpha1GetPackageRepositorySummariesResponse"
}
},
"401": {
"description": "Returned when the user does not have permission to access the resource.",
"schema": {}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "context.cluster",
"description": "Cluster. A cluster name can be provided to target a specific cluster if multiple\nclusters are configured, otherwise all clusters will be assumed.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "context.namespace",
"description": "Namespace. A namespace must be provided if the context of the operation is for a resource\nor resources in a particular namespace.\nFor requests to list items, not including a namespace here implies that the context\nfor the request is everything the requesting user can read, though the result can\nbe filtered by any filtering options of the request. Plugins may choose to return\nUnimplemented for some queries for which we do not yet have a need.",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"RepositoriesService"
]
},
"post": {
"operationId": "RepositoriesService_AddPackageRepository",
"responses": {
Expand Down Expand Up @@ -1345,6 +1385,46 @@
}
},
"/plugins/fluxv2/packages/v1alpha1/repositories": {
"get": {
"operationId": "FluxV2RepositoriesService_GetPackageRepositorySummaries",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1alpha1GetPackageRepositorySummariesResponse"
}
},
"401": {
"description": "Returned when the user does not have permission to access the resource.",
"schema": {}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "context.cluster",
"description": "Cluster. A cluster name can be provided to target a specific cluster if multiple\nclusters are configured, otherwise all clusters will be assumed.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "context.namespace",
"description": "Namespace. A namespace must be provided if the context of the operation is for a resource\nor resources in a particular namespace.\nFor requests to list items, not including a namespace here implies that the context\nfor the request is everything the requesting user can read, though the result can\nbe filtered by any filtering options of the request. Plugins may choose to return\nUnimplemented for some queries for which we do not yet have a need.",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"FluxV2RepositoriesService"
]
},
"post": {
"summary": "AddPackageRepository add an existing package repository to the set of ones already managed by the\n'fluxv2' plugin",
"operationId": "FluxV2RepositoriesService_AddPackageRepository",
Expand Down Expand Up @@ -3613,6 +3693,20 @@
"description": "Response for GetPackageRepositoryDetail",
"title": "GetPackageRepositoryDetailResponse"
},
"v1alpha1GetPackageRepositorySummariesResponse": {
"type": "object",
"properties": {
"packageRepositorySummaries": {
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1PackageRepositorySummary"
},
"title": "List of PackageRepositorySummary"
}
},
"description": "Response for GetPackageRepositorySummaries",
"title": "GetPackageRepositorySummariesResponse"
},
"v1alpha1GetResourcesResponse": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -4036,6 +4130,40 @@
"description": "Generic reasons why a package repository may be ready or not.\nThese should make sense across different packaging plugins.",
"title": "StatusReason"
},
"v1alpha1PackageRepositorySummary": {
"type": "object",
"properties": {
"packageRepoRef": {
"$ref": "#/definitions/v1alpha1PackageRepositoryReference",
"description": "A reference uniquely identifying the package repository."
},
"name": {
"type": "string",
"title": "A user-provided name for the package repository (e.g. bitnami)"
},
"description": {
"type": "string",
"description": "A user-provided description."
},
"namespaceScoped": {
"type": "boolean",
"description": "Whether this repository is global or namespace-scoped."
},
"type": {
"type": "string",
"title": "Package storage type"
},
"url": {
"type": "string",
"description": "URL identifying the package repository location."
},
"status": {
"$ref": "#/definitions/v1alpha1PackageRepositoryStatus",
"description": "current status of the repository which can include reconciliation\nstatus, where relevant."
}
},
"title": "PackageRepositorySummary"
},
"v1alpha1PackageRepositoryTlsConfig": {
"type": "object",
"properties": {
Expand Down
Loading

0 comments on commit b0b8dfc

Please sign in to comment.