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;