From 81b66a5166a08cfb5a7eb365b5c3ecd076d14a42 Mon Sep 17 00:00:00 2001 From: ricoberger Date: Mon, 6 Dec 2021 22:36:53 +0100 Subject: [PATCH] [harbor] Add support for multi-arch images This commits adds better support for multi-arch images, which are saved in Harbor. Until now, we only were showing the parent artifact for multi-arch image, without the build history and vulnerabilities. Now we are using the reference array from this artifact to get more information for all of the child artifacts. In this case one artifact represents one architecture/os for the image. To get the vulnerabilities for these artifacts we had to add the "X-Accept-Vulnerabilities" header to the requests. If this header isn't present the vulnerabilities are missing for multi-arch images. --- CHANGELOG.md | 1 + plugins/harbor/harbor.go | 24 +++ plugins/harbor/pkg/instance/instance.go | 19 ++ plugins/harbor/pkg/instance/structs.go | 14 +- .../src/components/panel/details/Details.tsx | 166 +++++++----------- .../src/components/panel/details/Overview.tsx | 107 +++++++++++ .../components/panel/details/Reference.tsx | 109 ++++++++++++ plugins/harbor/src/utils/interfaces.ts | 17 +- 8 files changed, 351 insertions(+), 106 deletions(-) create mode 100644 plugins/harbor/src/components/panel/details/Overview.tsx create mode 100644 plugins/harbor/src/components/panel/details/Reference.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 3752af1a8..c23c95204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#219](https://github.com/kobsio/kobs/pull/219): [azure] Add permissions for Azure plugin, so that access to resources and actions can be restricted based on resource groups. - [#220](https://github.com/kobsio/kobs/pull/220): [azure] Add auto formatting for the returned metrics of an container instance and fix the tooltip positioning in the metrics chart. - [#221](https://github.com/kobsio/kobs/pull/221): [azure] :warning: _Breaking change:_ :warning: Add support for kubernetes services and refactor various places in the Azure plugin. +- [#224](https://github.com/kobsio/kobs/pull/224): [harbor] Add support for multi-arch images. ### Fixed diff --git a/plugins/harbor/harbor.go b/plugins/harbor/harbor.go index 390b2a576..dbd820bc8 100644 --- a/plugins/harbor/harbor.go +++ b/plugins/harbor/harbor.go @@ -113,6 +113,29 @@ func (router *Router) getArtifacts(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, artifactsData) } +func (router *Router) getArtifact(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + projectName := r.URL.Query().Get("projectName") + repositoryName := r.URL.Query().Get("repositoryName") + artifactReference := r.URL.Query().Get("artifactReference") + + log.WithFields(logrus.Fields{"name": name, "projectName": projectName, "repositoryName": repositoryName, "artifactReference": artifactReference}).Tracef("getArtifact") + + i := router.getInstance(name) + if i == nil { + errresponse.Render(w, r, nil, http.StatusBadRequest, "Could not find instance name") + return + } + + artifact, err := i.GetArtifact(r.Context(), projectName, repositoryName, artifactReference) + if err != nil { + errresponse.Render(w, r, err, http.StatusInternalServerError, "Could not get artifact") + return + } + + render.JSON(w, r, artifact) +} + func (router *Router) getVulnerabilities(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") projectName := r.URL.Query().Get("projectName") @@ -193,6 +216,7 @@ func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Confi router.Get("/projects/{name}", router.getProjects) router.Get("/repositories/{name}", router.getRepositories) router.Get("/artifacts/{name}", router.getArtifacts) + router.Get("/artifact/{name}", router.getArtifact) router.Get("/vulnerabilities/{name}", router.getVulnerabilities) router.Get("/buildhistory/{name}", router.getBuildHistory) diff --git a/plugins/harbor/pkg/instance/instance.go b/plugins/harbor/pkg/instance/instance.go index e24689368..1687e6282 100644 --- a/plugins/harbor/pkg/instance/instance.go +++ b/plugins/harbor/pkg/instance/instance.go @@ -42,6 +42,8 @@ func (i *Instance) doRequest(ctx context.Context, url string) ([]byte, int64, er return nil, 0, err } + req.Header.Set("X-Accept-Vulnerabilities", "application/vnd.security.vulnerability.report; version=1.1, application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0") + resp, err := i.client.Do(req) if err != nil { return nil, 0, err @@ -148,6 +150,23 @@ func (i *Instance) GetArtifacts(ctx context.Context, projectName, repositoryName }, nil } +// GetArtifact returns a single artifact from the Harbor instance. +func (i *Instance) GetArtifact(ctx context.Context, projectName, repositoryName, artifactReference string) (*Artifact, error) { + repositoryName = url.PathEscape(repositoryName) + + body, _, err := i.doRequest(ctx, fmt.Sprintf("projects/%s/repositories/%s/artifacts/%s", projectName, repositoryName, artifactReference)) + if err != nil { + return nil, err + } + + var artifact Artifact + if err := json.Unmarshal(body, &artifact); err != nil { + return nil, err + } + + return &artifact, nil +} + // GetVulnerabilities returns a list of artifacts for a repository from the Harbor instance. func (i *Instance) GetVulnerabilities(ctx context.Context, projectName, repositoryName, artifactReference string) (map[string]Vulnerability, error) { repositoryName = url.PathEscape(repositoryName) diff --git a/plugins/harbor/pkg/instance/structs.go b/plugins/harbor/pkg/instance/structs.go index 431255305..19bac3600 100644 --- a/plugins/harbor/pkg/instance/structs.go +++ b/plugins/harbor/pkg/instance/structs.go @@ -85,7 +85,7 @@ type Artifact struct { ProjectID int64 `json:"project_id"` PullTime time.Time `json:"pull_time"` PushTime time.Time `json:"push_time"` - References interface{} `json:"references"` + References []ArtifactReference `json:"references"` RepositoryID int64 `json:"repository_id"` ScanOverview map[string]ArtifactScanOverview `json:"scan_overview"` Size int64 `json:"size"` @@ -93,6 +93,18 @@ type Artifact struct { Type string `json:"type"` } +type ArtifactReference struct { + ChildDigest string `json:"child_digest"` + ChildID int64 `json:"child_id"` + ParentID int64 `json:"parent_id"` + Platform struct { + OsFeatures []string `json:"OsFeatures"` + Architecture string `json:"architecture"` + Os string `json:"os"` + } `json:"platform"` + Urls []string `json:"urls"` +} + type ArtifactScanOverview struct { CompletePercent int64 `json:"complete_percent"` Duration int64 `json:"duration"` diff --git a/plugins/harbor/src/components/panel/details/Details.tsx b/plugins/harbor/src/components/panel/details/Details.tsx index 7f04d9779..825674620 100644 --- a/plugins/harbor/src/components/panel/details/Details.tsx +++ b/plugins/harbor/src/components/panel/details/Details.tsx @@ -8,14 +8,19 @@ import { DrawerHead, DrawerPanelBody, DrawerPanelContent, + Tab, + TabTitleText, + Tabs, Tooltip, } from '@patternfly/react-core'; +import React, { useState } from 'react'; import { TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import { CopyIcon } from '@patternfly/react-icons'; -import React from 'react'; import BuildHistory from './BuildHistory'; import { IArtifact } from '../../../utils/interfaces'; +import Overview from './Overview'; +import Reference from './Reference'; import { Title } from '@kobsio/plugin-core'; import Vulnerabilities from './Vulnerabilities'; import { formatTime } from '../../../utils/helpers'; @@ -37,6 +42,10 @@ const Details: React.FunctionComponent = ({ artifact, close, }: IDetailsProps) => { + const [activeTab, setActiveTab] = useState( + artifact.references && artifact.references.length > 0 ? artifact.references[0].child_digest : '', + ); + const copyPullCommand = (tag: string): void => { if (navigator.clipboard) { navigator.clipboard.writeText( @@ -98,113 +107,62 @@ const Details: React.FunctionComponent = ({ )} - {artifact.extra_attrs && ( + {artifact.references && artifact.references.length > 0 ? ( + setActiveTab(tabIndex.toString())} + className="pf-u-mt-md" + isFilled={true} + mountOnEnter={true} + > + {artifact.references.map((reference) => ( + + {reference.child_digest.substring(0, 15)} ({reference.platform.os}/{reference.platform.architecture} + ) + + } + > +
+ +
+
+ ))} +
+ ) : (
- - Overview - - - - {artifact.extra_attrs.created && ( - - Created - {formatTime(artifact.extra_attrs.created)} - - )} - {artifact.extra_attrs.author && ( - - Author - {artifact.extra_attrs.author} - - )} - {artifact.extra_attrs.architecture && ( - - Architecture - {artifact.extra_attrs.architecture} - - )} - {artifact.extra_attrs.os && ( - - OS - {artifact.extra_attrs.os} - - )} - {artifact.extra_attrs.config.Cmd && ( - - Cmd - {artifact.extra_attrs.config.Cmd.join('\n')} - - )} - {artifact.extra_attrs.config.Entrypoint && ( - - Entrypoint - {artifact.extra_attrs.config.Entrypoint.join('\n')} - - )} - {artifact.extra_attrs.config.Env && ( - - Env - {artifact.extra_attrs.config.Env.join('\n')} - - )} - {artifact.extra_attrs.config.ExposedPorts && ( - - Ports - - {Object.keys(artifact.extra_attrs.config.ExposedPorts).join('\n')} - - - )} - {artifact.extra_attrs.config.Labels && ( - - Labels - - {Object.keys(artifact.extra_attrs.config.Labels) - .map( - (key) => - `${key}=${ - artifact.extra_attrs.config.Labels && artifact.extra_attrs.config.Labels[key] - }`, - ) - .join('\n')} - - - )} - {artifact.extra_attrs.config.User && ( - - User - {artifact.extra_attrs.config.User} - - )} - {artifact.extra_attrs.config.WorkingDir && ( - - Workind Dir - {artifact.extra_attrs.config.WorkingDir} - - )} - - - - + {artifact.extra_attrs && ( +
+ +

 

+
+ )} + + +

 

+ +

 

)} - - -

 

- - -

 

); diff --git a/plugins/harbor/src/components/panel/details/Overview.tsx b/plugins/harbor/src/components/panel/details/Overview.tsx new file mode 100644 index 000000000..31c50fce3 --- /dev/null +++ b/plugins/harbor/src/components/panel/details/Overview.tsx @@ -0,0 +1,107 @@ +import { Card, CardBody, CardTitle } from '@patternfly/react-core'; +import { TableComposable, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table'; +import React from 'react'; + +import { formatBytes, formatTime } from '../../../utils/helpers'; +import { IArtifact } from '../../../utils/interfaces'; + +interface IOverviewProps { + artifact: IArtifact; +} + +const Overview: React.FunctionComponent = ({ artifact }: IOverviewProps) => { + return ( + + Overview + + + + {artifact.extra_attrs.created && ( + + Created + {formatTime(artifact.extra_attrs.created)} + + )} + {artifact.size && ( + + Size + {formatBytes(artifact.size)} + + )} + {artifact.extra_attrs.author && ( + + Author + {artifact.extra_attrs.author} + + )} + {artifact.extra_attrs.architecture && ( + + Architecture + {artifact.extra_attrs.architecture} + + )} + {artifact.extra_attrs.os && ( + + OS + {artifact.extra_attrs.os} + + )} + {artifact.extra_attrs.config.Cmd && ( + + Cmd + {artifact.extra_attrs.config.Cmd.join('\n')} + + )} + {artifact.extra_attrs.config.Entrypoint && ( + + Entrypoint + {artifact.extra_attrs.config.Entrypoint.join('\n')} + + )} + {artifact.extra_attrs.config.Env && ( + + Env + {artifact.extra_attrs.config.Env.join('\n')} + + )} + {artifact.extra_attrs.config.ExposedPorts && ( + + Ports + + {Object.keys(artifact.extra_attrs.config.ExposedPorts).join('\n')} + + + )} + {artifact.extra_attrs.config.Labels && ( + + Labels + + {Object.keys(artifact.extra_attrs.config.Labels) + .map( + (key) => + `${key}=${artifact.extra_attrs.config.Labels && artifact.extra_attrs.config.Labels[key]}`, + ) + .join('\n')} + + + )} + {artifact.extra_attrs.config.User && ( + + User + {artifact.extra_attrs.config.User} + + )} + {artifact.extra_attrs.config.WorkingDir && ( + + Workind Dir + {artifact.extra_attrs.config.WorkingDir} + + )} + + + + + ); +}; + +export default Overview; diff --git a/plugins/harbor/src/components/panel/details/Reference.tsx b/plugins/harbor/src/components/panel/details/Reference.tsx new file mode 100644 index 000000000..15649f692 --- /dev/null +++ b/plugins/harbor/src/components/panel/details/Reference.tsx @@ -0,0 +1,109 @@ +import { Alert, AlertActionLink, AlertVariant, Spinner } from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React from 'react'; + +import BuildHistory from './BuildHistory'; +import { IArtifact } from '../../../utils/interfaces'; +import Overview from './Overview'; +import Vulnerabilities from './Vulnerabilities'; + +interface IReferenceProps { + name: string; + projectName: string; + repositoryName: string; + artifactReference: string; +} + +const Reference: React.FunctionComponent = ({ + name, + projectName, + repositoryName, + artifactReference, +}: IReferenceProps) => { + const { isError, isLoading, error, data, refetch } = useQuery( + ['harbor/artifact', name, projectName, repositoryName, artifactReference], + async () => { + try { + const response = await fetch( + `/api/plugins/harbor/artifact/${name}?projectName=${projectName}&repositoryName=${repositoryName}&artifactReference=${artifactReference}`, + { + method: 'get', + }, + ); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }, + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ); + } + + if (!data) { + return null; + } + + return ( +
+ {data.extra_attrs && ( +
+ +

 

+
+ )} + + +

 

+ + +

 

+
+ ); +}; + +export default Reference; diff --git a/plugins/harbor/src/utils/interfaces.ts b/plugins/harbor/src/utils/interfaces.ts index 36398d974..0cb78ac68 100644 --- a/plugins/harbor/src/utils/interfaces.ts +++ b/plugins/harbor/src/utils/interfaces.ts @@ -115,7 +115,7 @@ export interface IArtifact { // eslint-disable-next-line @typescript-eslint/naming-convention push_time: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any - references: any; + references?: IArtifactReference[]; // eslint-disable-next-line @typescript-eslint/naming-convention repository_id: number; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -125,6 +125,21 @@ export interface IArtifact { type: string; } +export interface IArtifactReference { + // eslint-disable-next-line @typescript-eslint/naming-convention + child_digest: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + child_id: number; + // eslint-disable-next-line @typescript-eslint/naming-convention + parent_id: number; + platform: { + OsFeatures: string[]; + architecture: string; + os: string; + }; + urls: string[]; +} + export interface IArtifactScanOverview { // eslint-disable-next-line @typescript-eslint/naming-convention complete_percent: number;