Skip to content

Commit

Permalink
Add 'GetAvailablePackageSummaries' impl (#3784)
Browse files Browse the repository at this point in the history
* Add required files for the Carvel plugin impl

Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>

* Add 'GetAvailablePackageSummaries' impl

Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>

* Remove old tests. Fix TestGetClient test

Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>

* Add TestGetAvailablePackageSummaries

Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>

* Add 'GetAvailablePackageVersions' impl (#3785)

* Add 'GetAvailablePackageVersions' impl

Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>

* Add TestGetAvailablePackageVersions

Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>

* Changes after code review

Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>

* Changes after code review

Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
  • Loading branch information
antgamdia committed Nov 24, 2021
1 parent a4eaa66 commit f883009
Show file tree
Hide file tree
Showing 4 changed files with 648 additions and 0 deletions.
Expand Up @@ -11,3 +11,164 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package main

import (
"context"
"fmt"
"sync"

corev1 "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/packages/v1alpha1"
datapackagingv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apiserver/apis/datapackaging/v1alpha1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
log "k8s.io/klog/v2"
)

// GetAvailablePackageSummaries returns the available packages managed by the 'kapp_controller' plugin
func (s *Server) GetAvailablePackageSummaries(ctx context.Context, request *corev1.GetAvailablePackageSummariesRequest) (*corev1.GetAvailablePackageSummariesResponse, error) {
log.Infof("+kapp-controller GetAvailablePackageSummaries")

// Retrieve the proper parameters from the request
namespace := request.GetContext().GetNamespace()
cluster := request.GetContext().GetCluster()
pageSize := request.GetPaginationOptions().GetPageSize()
pageOffset, err := pageOffsetFromPageToken(request.GetPaginationOptions().GetPageToken())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unable to intepret page token %q: %v", request.GetPaginationOptions().GetPageToken(), err)
}
// Assume the default cluster if none is specified
if cluster == "" {
cluster = s.globalPackagingCluster
}
// fetch all the package metadatas
pkgMetadatas, err := s.getPkgMetadatas(ctx, cluster, namespace)
if err != nil {
return nil, errorByStatus("get", "PackageMetadata", "", err)
}

// paginate the list of results
availablePackageSummaries := make([]*corev1.AvailablePackageSummary, len(pkgMetadatas))

// create the waiting group for processing each item aynchronously
var wg sync.WaitGroup

// TODO(agamez): DRY up this logic (cf GetInstalledPackageSummaries)
if len(pkgMetadatas) > 0 {
startAt := -1
if pageSize > 0 {
startAt = int(pageSize) * pageOffset
}
for i, pkgMetadata := range pkgMetadatas {
wg.Add(1)
if startAt <= i {
go func(i int, pkgMetadata *datapackagingv1alpha1.PackageMetadata) error {
defer wg.Done()
// fetch the associated packages
// Use the field selector to return only Package CRs that match on the spec.refName.
// TODO(agamez): perhaps we better fetch all the packages and filter ourselves to reduce the k8s calls
fieldSelector := fmt.Sprintf("spec.refName=%s", pkgMetadata.Name)
pkgs, err := s.getPkgsWithFieldSelector(ctx, cluster, namespace, fieldSelector)
if err != nil {
return errorByStatus("get", "Package", pkgMetadata.Name, err)
}
pkgVersionsMap, err := getPkgVersionsMap(pkgs)
if err != nil {
return err
}

// generate the availablePackageSummary from the fetched information
availablePackageSummary, err := s.buildAvailablePackageSummary(pkgMetadata, pkgVersionsMap, cluster)
if err != nil {
return status.Errorf(codes.Internal, fmt.Sprintf("unable to create the AvailablePackageSummary: %v", err))
}

// append the availablePackageSummary to the slice
availablePackageSummaries[i] = availablePackageSummary
return nil
}(i, pkgMetadata)
}
// if we've reached the end of the page, stop iterating
if pageSize > 0 && len(availablePackageSummaries) == int(pageSize) {
break
}
}
}
wg.Wait() // Wait until each goroutine has finished

// TODO(agamez): the slice with make is filled with <nil>, in case of an error in the
// i goroutine, the i-th <nil> stub will remain. Check if 'errgroup' works here, but I haven't
// been able so far.
// An alternative is using channels to perform a fine-grained control... but not sure if it worths
// However, should we just return an error if so? See https://github.com/kubeapps/kubeapps/pull/3784#discussion_r754836475
// filter out <nil> values
availablePackageSummariesNilSafe := []*corev1.AvailablePackageSummary{}
categories := []string{}
for _, availablePackageSummary := range availablePackageSummaries {
if availablePackageSummary != nil {
availablePackageSummariesNilSafe = append(availablePackageSummariesNilSafe, availablePackageSummary)
categories = append(categories, availablePackageSummary.Categories...)

}
}
// if no results whatsoever, throw an error
if len(availablePackageSummariesNilSafe) == 0 {
return nil, status.Errorf(codes.NotFound, fmt.Sprintf("no available packages: %v", err))
}

// Only return a next page token if the request was for pagination and
// the results are a full page.
nextPageToken := ""
if pageSize > 0 && len(availablePackageSummariesNilSafe) == int(pageSize) {
nextPageToken = fmt.Sprintf("%d", pageOffset+1)
}
response := &corev1.GetAvailablePackageSummariesResponse{
AvailablePackageSummaries: availablePackageSummariesNilSafe,
// TODO(agamez): populate this field
Categories: categories,
NextPageToken: nextPageToken,
}
return response, nil
}

// GetAvailablePackageVersions returns the package versions managed by the 'kapp_controller' plugin
func (s *Server) GetAvailablePackageVersions(ctx context.Context, request *corev1.GetAvailablePackageVersionsRequest) (*corev1.GetAvailablePackageVersionsResponse, error) {
log.Infof("+kapp-controller GetAvailablePackageVersions")

// Retrieve the proper parameters from the request
namespace := request.GetAvailablePackageRef().GetContext().GetNamespace()
cluster := request.GetAvailablePackageRef().GetContext().GetCluster()
identifier := request.GetAvailablePackageRef().GetIdentifier()

// Validate the request
if namespace == "" || identifier == "" {
return nil, status.Errorf(codes.InvalidArgument, "Required context or identifier not provided")
}

if cluster == "" {
cluster = s.globalPackagingCluster
}

// Use the field selector to return only Package CRs that match on the spec.refName.
fieldSelector := fmt.Sprintf("spec.refName=%s", identifier)
pkgs, err := s.getPkgsWithFieldSelector(ctx, cluster, namespace, fieldSelector)
if err != nil {
return nil, errorByStatus("get", "Package", "", err)
}
pkgVersionsMap, err := getPkgVersionsMap(pkgs)
if err != nil {
return nil, err
}

// TODO(minelson): support configurable version summary for kapp-controller pkgs
// as already done for Helm (see #3588 for more info).
versions := make([]*corev1.PackageAppVersion, len(pkgVersionsMap[identifier]))
for i, v := range pkgVersionsMap[identifier] {
versions[i] = &corev1.PackageAppVersion{
PkgVersion: v.version.String(),
}
}

return &corev1.GetAvailablePackageVersionsResponse{
PackageAppVersions: versions,
}, nil
}
Expand Up @@ -11,3 +11,49 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package main

import (
"fmt"
"strings"

corev1 "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/packages/v1alpha1"
datapackagingv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apiserver/apis/datapackaging/v1alpha1"
)

func (s *Server) buildAvailablePackageSummary(pkgMetadata *datapackagingv1alpha1.PackageMetadata, pkgVersionsMap map[string][]pkgSemver, cluster string) (*corev1.AvailablePackageSummary, error) {
var iconStringBuilder strings.Builder

// get the versions associated with the package
versions := pkgVersionsMap[pkgMetadata.Name]
if len(versions) == 0 {
return nil, fmt.Errorf("no package versions for the package %q", pkgMetadata.Name)
}

// Carvel uses base64-encoded SVG data for IconSVGBase64, whereas we need
// a url, so convert to a data-url.
if pkgMetadata.Spec.IconSVGBase64 != "" {
iconStringBuilder.WriteString("data:image/svg+xml;base64,")
iconStringBuilder.WriteString(pkgMetadata.Spec.IconSVGBase64)
}

availablePackageSummary := &corev1.AvailablePackageSummary{
AvailablePackageRef: &corev1.AvailablePackageReference{
Context: &corev1.Context{
Cluster: cluster,
Namespace: pkgMetadata.Namespace,
},
Plugin: &pluginDetail,
Identifier: pkgMetadata.Name,
},
Name: pkgMetadata.Name,
LatestVersion: &corev1.PackageAppVersion{
PkgVersion: versions[0].version.String(),
},
IconUrl: iconStringBuilder.String(),
DisplayName: pkgMetadata.Spec.DisplayName,
ShortDescription: pkgMetadata.Spec.ShortDescription,
Categories: pkgMetadata.Spec.Categories,
}

return availablePackageSummary, nil
}
Expand Up @@ -219,6 +219,8 @@ func (s *Server) getPkgsWithFieldSelector(ctx context.Context, cluster, namespac
if fieldSelector != "" {
listOptions.FieldSelector = fieldSelector
}
// TODO(agamez): this function takes way too long (1-2 seconds!). Try to reduce it
// More context at: https://github.com/kubeapps/kubeapps/pull/3784#discussion_r756259504
unstructured, err := resource.List(ctx, listOptions)
if err != nil {
return nil, err
Expand Down

0 comments on commit f883009

Please sign in to comment.