mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:51:57 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc97541a28 | ||
|
|
e1e2029d29 | ||
|
|
17871a4647 | ||
|
|
d24e8fe7f3 | ||
|
|
162d3836da | ||
|
|
8cf3916c44 | ||
|
|
72ab1f818b | ||
|
|
fa826c30dc | ||
|
|
a88bc4d2d8 | ||
|
|
d498442148 | ||
|
|
0f4cbff0ca | ||
|
|
1a463ba167 | ||
|
|
074af1bda8 | ||
|
|
9522c46e6b | ||
|
|
556b880329 | ||
|
|
3117b03be9 | ||
|
|
9cf00da25a | ||
|
|
6b8d334563 | ||
|
|
21247e44ac | ||
|
|
20adc60d67 | ||
|
|
308624f144 | ||
|
|
174c08c8c2 | ||
|
|
8ee41490b9 | ||
|
|
344169fd47 |
@ -501,7 +501,7 @@ load("//dev:schema_migrations.bzl", "schema_migrations")
|
||||
|
||||
schema_migrations(
|
||||
name = "schemas_migrations",
|
||||
updated_at = "2024-07-10 12:15",
|
||||
updated_at = "2024-07-10 23:24",
|
||||
)
|
||||
|
||||
# wolfi images setup ================================
|
||||
|
||||
@ -245,6 +245,7 @@ TEST_BUILD_DEPS = [
|
||||
|
||||
vitest_test(
|
||||
name = "unit_tests",
|
||||
timeout = "moderate",
|
||||
bin = vitest_bin,
|
||||
chdir = package_name(),
|
||||
data = SRCS + BUILD_DEPS + CONFIGS + TESTS + TEST_BUILD_DEPS,
|
||||
|
||||
@ -15,7 +15,10 @@ const config: PlaywrightTestConfig = {
|
||||
: undefined,
|
||||
reporter: 'list',
|
||||
// note: if you proxy into a locally running vite preview, you may have to raise this to 60 seconds
|
||||
timeout: 5_000,
|
||||
timeout: process.env.BAZEL ? 60_000 : 30_000,
|
||||
expect: {
|
||||
timeout: process.env.BAZEL ? 20_000 : 5_000,
|
||||
},
|
||||
use: {
|
||||
baseURL: `http://localhost:${PORT}`,
|
||||
},
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
>
|
||||
<div class="sidebar-navigation-header">
|
||||
<button class="close-button" on:click={() => (sidebarNavigationOpen = false)}>
|
||||
<Icon icon={ILucideX} aria-label="Close sidebar navigation" />
|
||||
<Icon icon={ILucideX} />
|
||||
</button>
|
||||
|
||||
<a href="/search" class="logo-link">
|
||||
|
||||
@ -166,7 +166,7 @@ test.describe('file sidebar', () => {
|
||||
await expect(page.getByText(/Child error/)).toBeVisible()
|
||||
})
|
||||
|
||||
test('error handling non-existing directory -> root', async ({ page, sg }) => {
|
||||
test.skip('error handling non-existing directory -> root', async ({ page, sg }) => {
|
||||
// Here we expect the sidebar to show an error message, and after navigigating
|
||||
// to an existing directory, the directory contents
|
||||
sg.mockOperations({
|
||||
@ -248,9 +248,8 @@ test('history panel', async ({ page, sg }) => {
|
||||
await expect(page.getByText('Test commit')).toBeHidden()
|
||||
})
|
||||
|
||||
test('file popover', async ({ page, sg }, testInfo) => {
|
||||
// Test needs more time to teardown
|
||||
test.setTimeout(testInfo.timeout * 3000)
|
||||
test('file popover', async ({ page, sg }) => {
|
||||
test.slow()
|
||||
|
||||
await page.goto(`/${repoName}`)
|
||||
|
||||
|
||||
@ -35,6 +35,8 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
|
||||
accessTokensExpirationDaysOptions: [7, 14, 30, 60, 90],
|
||||
allowSignup: true,
|
||||
batchChangesEnabled: true,
|
||||
applianceUpdateTarget: '',
|
||||
applianceMenuTarget: '',
|
||||
batchChangesDisableWebhooksWarning: false,
|
||||
batchChangesWebhookLogsEnabled: true,
|
||||
executorsEnabled: false,
|
||||
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import { CodyWebHistory, CodyWebChatProvider } from 'cody-web-experimental'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
|
||||
import { Badge, ButtonLink, PageHeader, Text } from '@sourcegraph/wildcard'
|
||||
import { ButtonLink, PageHeader, ProductStatusBadge, Text } from '@sourcegraph/wildcard'
|
||||
|
||||
import { Page } from '../../../components/Page'
|
||||
import { PageTitle } from '../../../components/PageTitle'
|
||||
@ -31,7 +31,11 @@ export const NewCodyChatPage: FC<NewCodyChatPageProps> = props => {
|
||||
<CodyPageHeader isSourcegraphDotCom={isSourcegraphDotCom} className={styles.pageHeader} />
|
||||
|
||||
<div className={styles.chatContainer}>
|
||||
<CodyWebChatProvider accessToken="" serverEndpoint={window.location.origin}>
|
||||
<CodyWebChatProvider
|
||||
accessToken=""
|
||||
serverEndpoint={window.location.origin}
|
||||
customHeaders={window.context.xhrHeaders}
|
||||
>
|
||||
<CodyWebHistory>
|
||||
{history => (
|
||||
<div className={styles.chatHistory}>
|
||||
@ -95,9 +99,7 @@ const CodyPageHeader: FC<CodyPageHeaderProps> = props => {
|
||||
<PageHeader.Breadcrumb icon={CodyColorIcon}>
|
||||
<div className="d-inline-flex align-items-center">
|
||||
Cody Chat
|
||||
<Badge variant="info" className="ml-2">
|
||||
Experimental
|
||||
</Badge>
|
||||
<ProductStatusBadge status="beta" className="ml-2" />
|
||||
</div>
|
||||
</PageHeader.Breadcrumb>
|
||||
</PageHeader.Heading>
|
||||
|
||||
@ -4,7 +4,7 @@ import { mdiClose } from '@mdi/js'
|
||||
|
||||
import { CodyLogo } from '@sourcegraph/cody-ui'
|
||||
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
|
||||
import { Alert, Badge, Button, H4, Icon, LoadingSpinner } from '@sourcegraph/wildcard'
|
||||
import { Alert, Button, H4, Icon, LoadingSpinner, ProductStatusBadge } from '@sourcegraph/wildcard'
|
||||
|
||||
import styles from './NewCodySidebar.module.scss'
|
||||
|
||||
@ -32,7 +32,7 @@ export const NewCodySidebar: FC<NewCodySidebarProps> = props => {
|
||||
<CodyLogo />
|
||||
Cody
|
||||
<div className="ml-2">
|
||||
<Badge variant="info">Experimental</Badge>
|
||||
<ProductStatusBadge status="beta" />
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="icon" aria-label="Close" onClick={onClose}>
|
||||
|
||||
@ -48,6 +48,7 @@ export const NewCodySidebarWebChat: FC<NewCodySidebarWebChatProps> = memo(functi
|
||||
chatID={chatID}
|
||||
initialContext={contextInfo}
|
||||
serverEndpoint={window.location.origin}
|
||||
customHeaders={window.context.xhrHeaders}
|
||||
onNewChatCreated={handleNewChatCreated}
|
||||
>
|
||||
<ChatUi />
|
||||
|
||||
@ -383,6 +383,7 @@ const AddToken: FC<AddTokenProps> = ({
|
||||
authenticatedUser={user as unknown as AuthenticatedUser}
|
||||
minimizedMode={true}
|
||||
kind={kind}
|
||||
externalServiceURL={externalServiceURL}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -23,16 +23,18 @@ interface BatchChangesCreateGitHubAppPageProps {
|
||||
authenticatedUser: AuthenticatedUser
|
||||
minimizedMode?: boolean
|
||||
kind: GitHubAppKind
|
||||
externalServiceURL?: string
|
||||
}
|
||||
|
||||
export const BatchChangesCreateGitHubAppPage: FC<BatchChangesCreateGitHubAppPageProps> = ({
|
||||
minimizedMode,
|
||||
kind,
|
||||
authenticatedUser,
|
||||
externalServiceURL,
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const baseURL = searchParams.get('baseURL')
|
||||
const baseURL = externalServiceURL || searchParams.get('baseURL')
|
||||
|
||||
const isGitHubAppKindCredential = kind === GitHubAppKind.USER_CREDENTIAL || kind === GitHubAppKind.SITE_CREDENTIAL
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ export const AdminSidebarItems: StoryFn = () => (
|
||||
batchChangesExecutionEnabled={true}
|
||||
batchChangesWebhookLogsEnabled={true}
|
||||
codeInsightsEnabled={true}
|
||||
applianceUpdateTarget=""
|
||||
endUserOnboardingEnabled={false}
|
||||
/>
|
||||
<SiteAdminSidebar
|
||||
@ -48,6 +49,7 @@ export const AdminSidebarItems: StoryFn = () => (
|
||||
batchChangesExecutionEnabled={true}
|
||||
batchChangesWebhookLogsEnabled={true}
|
||||
codeInsightsEnabled={true}
|
||||
applianceUpdateTarget=""
|
||||
endUserOnboardingEnabled={false}
|
||||
/>
|
||||
<SiteAdminSidebar
|
||||
@ -58,6 +60,7 @@ export const AdminSidebarItems: StoryFn = () => (
|
||||
batchChangesExecutionEnabled={false}
|
||||
batchChangesWebhookLogsEnabled={false}
|
||||
codeInsightsEnabled={true}
|
||||
applianceUpdateTarget=""
|
||||
endUserOnboardingEnabled={false}
|
||||
/>
|
||||
<SiteAdminSidebar
|
||||
@ -68,6 +71,7 @@ export const AdminSidebarItems: StoryFn = () => (
|
||||
batchChangesExecutionEnabled={true}
|
||||
batchChangesWebhookLogsEnabled={true}
|
||||
codeInsightsEnabled={false}
|
||||
applianceUpdateTarget=""
|
||||
endUserOnboardingEnabled={false}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@ -27,6 +27,8 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
|
||||
accessTokensExpirationDaysOptions: [7, 30, 60, 90],
|
||||
allowSignup: false,
|
||||
batchChangesEnabled: true,
|
||||
applianceUpdateTarget: '',
|
||||
applianceMenuTarget: '',
|
||||
batchChangesDisableWebhooksWarning: false,
|
||||
batchChangesWebhookLogsEnabled: true,
|
||||
codeInsightsEnabled: true,
|
||||
|
||||
@ -196,6 +196,12 @@ export interface SourcegraphContext extends Pick<Required<SiteConfiguration>, 'e
|
||||
|
||||
batchChangesWebhookLogsEnabled: boolean
|
||||
|
||||
/**
|
||||
* Whether this sourcegraph instance is managed by Appliance
|
||||
*/
|
||||
applianceUpdateTarget: string
|
||||
applianceMenuTarget: string
|
||||
|
||||
/**
|
||||
* Whether Cody is enabled on this instance. Check
|
||||
* {@link SourcegraphContext.codyEnabledForCurrentUser} to see whether Cody is enabled for the
|
||||
|
||||
@ -227,6 +227,11 @@ export const UserNavItem: FC<UserNavItemProps> = props => {
|
||||
Site admin
|
||||
</MenuLink>
|
||||
)}
|
||||
{authenticatedUser.siteAdmin && window.context.applianceMenuTarget !== '' && (
|
||||
<MenuLink as={Link} to={window.context.applianceMenuTarget}>
|
||||
Appliance
|
||||
</MenuLink>
|
||||
)}
|
||||
<MenuLink as={Link} to="/help" target="_blank" rel="noopener">
|
||||
Help <Icon aria-hidden={true} svgPath={mdiOpenInNew} />
|
||||
</MenuLink>
|
||||
|
||||
@ -48,17 +48,19 @@ export const useBlameHunks = ({
|
||||
const [isBlameVisible] = useBlameVisibility(isPackage)
|
||||
const shouldFetchBlame = isBlameVisible
|
||||
|
||||
const hunks = useObservable(
|
||||
useMemo(
|
||||
() =>
|
||||
shouldFetchBlame
|
||||
? fetchBlameWithExternalURLs({ revision, repoName, filePath })
|
||||
: of({ current: undefined, externalURLs: undefined }),
|
||||
[shouldFetchBlame, revision, repoName, filePath]
|
||||
)
|
||||
const stream = useMemo(
|
||||
() =>
|
||||
shouldFetchBlame
|
||||
? fetchBlameWithExternalURLs({ revision, repoName, filePath })
|
||||
: of({ current: undefined, externalURLs: undefined }),
|
||||
[shouldFetchBlame, revision, repoName, filePath]
|
||||
)
|
||||
|
||||
return hunks || { current: undefined, externalURLs: undefined }
|
||||
try {
|
||||
const hunks = useObservable(stream)
|
||||
return hunks || { current: undefined, externalURLs: undefined }
|
||||
} catch (error) {
|
||||
return { message: error.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRepositoryData(repoName: string): Promise<Omit<BlameHunkData, 'current'>> {
|
||||
|
||||
@ -157,7 +157,7 @@ function fetchRawBlameHunks(repoName: string, revision: string, filePath: string
|
||||
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
||||
return
|
||||
}
|
||||
subscriber.error(new Error('request for blame data failed: ' + (await response.text())))
|
||||
throw new Error('request for blame data failed: ' + (await response.text()))
|
||||
},
|
||||
onmessage(event) {
|
||||
if (event.event === 'hunk') {
|
||||
@ -165,8 +165,8 @@ function fetchRawBlameHunks(repoName: string, revision: string, filePath: string
|
||||
subscriber.next(rawHunks)
|
||||
}
|
||||
},
|
||||
onerror(event) {
|
||||
subscriber.error(event)
|
||||
onerror(err) {
|
||||
throw err
|
||||
},
|
||||
}).then(
|
||||
() => subscriber.complete(),
|
||||
|
||||
@ -50,7 +50,13 @@ export class OccurrenceIndex extends Array<Occurrence> {
|
||||
previousEndline = current.range.end.line
|
||||
}
|
||||
|
||||
super(...nonOverlappingOccurrences(occurrences))
|
||||
// CAUTION: Do not "optimize" this to super(...nonOverlappingOccurrences(occurrences))
|
||||
// as Chrome will push all elements to a stack, and potentially trigger a stack overflow.
|
||||
// Similar bug in Nodejs: https://github.com/nodejs/node/issues/16870
|
||||
super()
|
||||
for (const occ of nonOverlappingOccurrences(occurrences)) {
|
||||
this.push(occ)
|
||||
}
|
||||
this.lineIndex = lineIndex
|
||||
}
|
||||
|
||||
|
||||
@ -268,6 +268,7 @@ export const routes: RouteObject[] = [
|
||||
sideBarGroups={props.siteAdminSideBarGroups}
|
||||
overviewComponents={props.siteAdminOverviewComponents}
|
||||
codeInsightsEnabled={window.context.codeInsightsEnabled}
|
||||
applianceUpdateTarget={window.context.applianceUpdateTarget}
|
||||
telemetryRecorder={props.platformContext.telemetryRecorder}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -59,6 +59,7 @@ export interface SiteAdminAreaRouteContext
|
||||
overviewComponents: readonly React.ComponentType<React.PropsWithChildren<{}>>[]
|
||||
|
||||
codeInsightsEnabled: boolean
|
||||
applianceUpdateTarget: string
|
||||
|
||||
endUserOnboardingEnabled: boolean
|
||||
}
|
||||
@ -77,6 +78,7 @@ interface SiteAdminAreaProps
|
||||
authenticatedUser: AuthenticatedUser
|
||||
isSourcegraphDotCom: boolean
|
||||
codeInsightsEnabled: boolean
|
||||
applianceUpdateTarget: string
|
||||
}
|
||||
|
||||
const sourcegraphOperatorSiteAdminMaintenanceBlockItems = new Set([
|
||||
@ -142,6 +144,7 @@ const AuthenticatedSiteAdminArea: React.FunctionComponent<React.PropsWithChildre
|
||||
telemetryService: props.telemetryService,
|
||||
telemetryRecorder: props.telemetryRecorder,
|
||||
codeInsightsEnabled: props.codeInsightsEnabled,
|
||||
applianceUpdateTarget: props.applianceUpdateTarget,
|
||||
endUserOnboardingEnabled,
|
||||
}
|
||||
|
||||
@ -161,6 +164,7 @@ const AuthenticatedSiteAdminArea: React.FunctionComponent<React.PropsWithChildre
|
||||
batchChangesExecutionEnabled={props.batchChangesExecutionEnabled}
|
||||
batchChangesWebhookLogsEnabled={props.batchChangesWebhookLogsEnabled}
|
||||
codeInsightsEnabled={props.codeInsightsEnabled}
|
||||
applianceUpdateTarget={props.applianceUpdateTarget}
|
||||
endUserOnboardingEnabled={endUserOnboardingEnabled}
|
||||
/>
|
||||
<div className="flex-bounded">
|
||||
|
||||
@ -15,6 +15,7 @@ export interface SiteAdminSideBarGroupContext extends BatchChangesProps {
|
||||
isSourcegraphDotCom: boolean
|
||||
codeInsightsEnabled: boolean
|
||||
endUserOnboardingEnabled: boolean
|
||||
applianceUpdateTarget: string
|
||||
}
|
||||
|
||||
export interface SiteAdminSideBarGroup extends NavGroupDescriptor<SiteAdminSideBarGroupContext> {}
|
||||
|
||||
@ -135,6 +135,12 @@ const maintenanceGroup: SiteAdminSideBarGroup = {
|
||||
{
|
||||
label: maintenanceGroupUpdatesItemLabel,
|
||||
to: '/site-admin/updates',
|
||||
condition: ({ applianceUpdateTarget }) => applianceUpdateTarget === '',
|
||||
},
|
||||
{
|
||||
label: maintenanceGroupUpdatesItemLabel,
|
||||
to: window.context.applianceUpdateTarget,
|
||||
condition: ({ applianceUpdateTarget }) => applianceUpdateTarget !== '',
|
||||
},
|
||||
{
|
||||
label: 'Documentation',
|
||||
|
||||
@ -11,7 +11,9 @@ go_library(
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//internal/appliance",
|
||||
"//internal/appliance/healthchecker",
|
||||
"//internal/appliance/reconciler",
|
||||
"//internal/appliance/selfupdate",
|
||||
"//internal/appliance/v1:appliance",
|
||||
"//internal/debugserver",
|
||||
"//internal/env",
|
||||
@ -23,6 +25,7 @@ go_library(
|
||||
"//lib/errors",
|
||||
"@com_github_sourcegraph_log//:log",
|
||||
"@com_github_sourcegraph_log_logr//:logr",
|
||||
"@io_k8s_apimachinery//pkg/types",
|
||||
"@io_k8s_client_go//rest",
|
||||
"@io_k8s_client_go//tools/clientcmd",
|
||||
"@io_k8s_client_go//util/homedir",
|
||||
|
||||
@ -16,13 +16,15 @@ import (
|
||||
type Config struct {
|
||||
env.BaseConfig
|
||||
|
||||
k8sConfig *rest.Config
|
||||
metrics metricsConfig
|
||||
grpc grpcConfig
|
||||
http httpConfig
|
||||
namespace string
|
||||
relregEndpoint string
|
||||
applianceVersion string
|
||||
k8sConfig *rest.Config
|
||||
metrics metricsConfig
|
||||
grpc grpcConfig
|
||||
http httpConfig
|
||||
namespace string
|
||||
relregEndpoint string
|
||||
applianceVersion string
|
||||
selfDeploymentName string
|
||||
noResourceRestrictions string
|
||||
}
|
||||
|
||||
func (c *Config) Load() {
|
||||
@ -43,10 +45,12 @@ func (c *Config) Load() {
|
||||
c.metrics.addr = c.Get("APPLIANCE_METRICS_ADDR", ":8734", "Appliance metrics server address.")
|
||||
c.metrics.secure = c.GetBool("APPLIANCE_METRICS_SECURE", "false", "Appliance metrics server uses https.")
|
||||
c.grpc.addr = c.Get("APPLIANCE_GRPC_ADDR", ":9000", "Appliance gRPC address.")
|
||||
c.http.addr = c.Get("APPLIANCE_HTTP_ADDR", ":8080", "Appliance http address.")
|
||||
c.http.addr = c.Get("APPLIANCE_HTTP_ADDR", ":8888", "Appliance http address.")
|
||||
c.namespace = c.Get("APPLIANCE_NAMESPACE", "default", "Namespace to monitor.")
|
||||
c.applianceVersion = c.Get("APPLIANCE_VERSION", version.Version(), "Version tag for the running appliance.")
|
||||
c.selfDeploymentName = c.Get("APPLIANCE_DEPLOYMENT_NAME", "", "Own deployment name for self-update. Default is to disable self-update.")
|
||||
c.relregEndpoint = c.Get("RELEASE_REGISTRY_ENDPOINT", releaseregistry.Endpoint, "Release registry endpoint.")
|
||||
c.noResourceRestrictions = c.Get("APPLIANCE_NO_RESOURCE_RESTRICTIONS", "false", "Remove all resource requests and limits from deployed resources. Only recommended for local development.")
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
|
||||
@ -6,11 +6,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@ -18,8 +20,11 @@ import (
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
sglogr "github.com/sourcegraph/log/logr"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance"
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/healthchecker"
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/reconciler"
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/selfupdate"
|
||||
pb "github.com/sourcegraph/sourcegraph/internal/appliance/v1"
|
||||
"github.com/sourcegraph/sourcegraph/internal/grpc/defaults"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
@ -44,7 +49,14 @@ func Start(ctx context.Context, observationCtx *observation.Context, ready servi
|
||||
|
||||
relregClient := releaseregistry.NewClient(config.relregEndpoint)
|
||||
|
||||
app, err := appliance.NewAppliance(k8sClient, relregClient, config.applianceVersion, config.namespace, logger)
|
||||
noResourceRestrictions := false
|
||||
noResourceRestrictions, err = strconv.ParseBool(config.noResourceRestrictions)
|
||||
if err != nil {
|
||||
logger.Error("parsing APPLIANCE_NO_RESOURCE_RESTRICTIONS as bool", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
app, err := appliance.NewAppliance(k8sClient, relregClient, config.applianceVersion, config.namespace, noResourceRestrictions, logger)
|
||||
if err != nil {
|
||||
logger.Error("failed to create appliance", log.Error(err))
|
||||
return err
|
||||
@ -67,10 +79,13 @@ func Start(ctx context.Context, observationCtx *observation.Context, ready servi
|
||||
return err
|
||||
}
|
||||
|
||||
beginHealthCheckLoop := make(chan struct{})
|
||||
|
||||
if err = (&reconciler.Reconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Recorder: mgr.GetEventRecorderFor("sourcegraph-appliance"),
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Recorder: mgr.GetEventRecorderFor("sourcegraph-appliance"),
|
||||
BeginHealthCheckLoop: beginHealthCheckLoop,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
logger.Error("unable to create the appliance controller", log.Error(err))
|
||||
return err
|
||||
@ -92,6 +107,26 @@ func Start(ctx context.Context, observationCtx *observation.Context, ready servi
|
||||
|
||||
grpcServer := makeGRPCServer(logger, app)
|
||||
|
||||
selfUpdater := &selfupdate.SelfUpdate{
|
||||
Interval: time.Hour,
|
||||
Logger: logger.Scoped("SelfUpdate"),
|
||||
K8sClient: k8sClient,
|
||||
RelregClient: relregClient,
|
||||
DeploymentNames: config.selfDeploymentName,
|
||||
Namespace: config.namespace,
|
||||
}
|
||||
|
||||
probe := &healthchecker.PodProbe{K8sClient: k8sClient}
|
||||
healthChecker := &healthchecker.HealthChecker{
|
||||
Probe: probe,
|
||||
K8sClient: k8sClient,
|
||||
Logger: logger.Scoped("HealthChecker"),
|
||||
|
||||
ServiceName: types.NamespacedName{Name: "sourcegraph-frontend", Namespace: config.namespace},
|
||||
Interval: time.Minute,
|
||||
Graceperiod: time.Minute,
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
ctx = shutdownOnSignal(ctx)
|
||||
|
||||
@ -119,6 +154,18 @@ func Start(ctx context.Context, observationCtx *observation.Context, ready servi
|
||||
}
|
||||
return nil
|
||||
})
|
||||
g.Go(func() error {
|
||||
if err := healthChecker.ManageIngressFacingService(ctx, beginHealthCheckLoop, "app=sourcegraph-frontend", config.namespace); err != nil {
|
||||
logger.Error("problem running HealthChecker", log.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if config.selfDeploymentName != "" {
|
||||
g.Go(func() error {
|
||||
return selfUpdater.Loop(ctx)
|
||||
})
|
||||
}
|
||||
g.Go(func() error {
|
||||
<-ctx.Done()
|
||||
grpcServer.GracefulStop()
|
||||
|
||||
65
cmd/customer-2315/BUILD.bazel
Normal file
65
cmd/customer-2315/BUILD.bazel
Normal file
@ -0,0 +1,65 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
load("@rules_pkg//:pkg.bzl", "pkg_tar")
|
||||
load("@container_structure_test//:defs.bzl", "container_structure_test")
|
||||
load("//dev:oci_defs.bzl", "image_repository", "oci_image", "oci_push", "oci_tarball")
|
||||
|
||||
go_library(
|
||||
name = "customer-2315_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/cmd/customer-2315",
|
||||
tags = [TAG_CODY_PRIME],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"@com_github_google_uuid//:uuid",
|
||||
"@com_github_sourcegraph_log//:log",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "customer-2315",
|
||||
embed = [":customer-2315_lib"],
|
||||
tags = [TAG_CODY_PRIME],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "tar_customer-2315",
|
||||
srcs = [":customer-2315"],
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "image",
|
||||
base = "//wolfi-images/sourcegraph-base:base_image",
|
||||
entrypoint = [
|
||||
"/sbin/tini",
|
||||
"--",
|
||||
"/customer-2315",
|
||||
],
|
||||
tars = [":tar_customer-2315"],
|
||||
user = "sourcegraph",
|
||||
)
|
||||
|
||||
oci_tarball(
|
||||
name = "image_tarball",
|
||||
image = ":image",
|
||||
repo_tags = ["customer-2315:candidate"],
|
||||
)
|
||||
|
||||
container_structure_test(
|
||||
name = "image_test",
|
||||
timeout = "short",
|
||||
configs = ["image_test.yaml"],
|
||||
driver = "docker",
|
||||
image = ":image",
|
||||
tags = [
|
||||
"exclusive",
|
||||
"requires-network",
|
||||
TAG_CODY_PRIME,
|
||||
],
|
||||
)
|
||||
|
||||
oci_push(
|
||||
name = "candidate_push",
|
||||
image = ":image",
|
||||
repository = image_repository("customer-2315"),
|
||||
)
|
||||
15
cmd/customer-2315/image_test.yaml
Normal file
15
cmd/customer-2315/image_test.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
schemaVersion: "2.0.0"
|
||||
|
||||
commandTests:
|
||||
- name: "not running as root"
|
||||
command: "/usr/bin/id"
|
||||
args:
|
||||
- -u
|
||||
excludedOutput: ["^0"]
|
||||
exitCode: 0
|
||||
- name: "validate /customer-2315 file exists and is executable"
|
||||
command: "test"
|
||||
args:
|
||||
- "-x"
|
||||
- "/customer-2315"
|
||||
exitCode: 0
|
||||
224
cmd/customer-2315/main.go
Normal file
224
cmd/customer-2315/main.go
Normal file
@ -0,0 +1,224 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProxyServer struct {
|
||||
accessToken string
|
||||
tokenMutex sync.RWMutex
|
||||
client *http.Client
|
||||
azureEndpoint *url.URL
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) readSecretFile(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(data)), nil
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) generateHeaders(bearerToken string) map[string]string {
|
||||
return map[string]string{
|
||||
"correlationId": uuid.New().String(),
|
||||
"dataClassification": "sensitive",
|
||||
"dataSource": "internet",
|
||||
"Authorization": "Bearer " + bearerToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) updateAccessToken() {
|
||||
for {
|
||||
token, err := ps.getAccessToken()
|
||||
if err != nil {
|
||||
ps.logger.Fatal("Error getting access token: %v", log.Error(err))
|
||||
} else {
|
||||
ps.tokenMutex.Lock()
|
||||
ps.accessToken = token
|
||||
ps.tokenMutex.Unlock()
|
||||
ps.logger.Info("Access token updated")
|
||||
}
|
||||
time.Sleep(1 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) initializeAzureEndpoint() {
|
||||
var err error
|
||||
azure_endpoint, err := ps.readSecretFile("/run/secrets/azure_endpoint")
|
||||
if err != nil {
|
||||
ps.logger.Fatal("error reading OAUTH_URL: %v", log.Error(err))
|
||||
}
|
||||
ps.azureEndpoint, err = url.Parse(azure_endpoint)
|
||||
if err != nil {
|
||||
ps.logger.Fatal("Invalid AZURE_ENDPOINT: %v", log.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) initializeClient() {
|
||||
ps.client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 400,
|
||||
MaxIdleConnsPerHost: 400,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
},
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) getAccessToken() (string, error) {
|
||||
url, err := ps.readSecretFile("/run/secrets/oauth_url")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading OAUTH_URL: %v", err)
|
||||
}
|
||||
clientID, err := ps.readSecretFile("/run/secrets/client_id")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading CLIENT_ID: %v", err)
|
||||
}
|
||||
clientSecret, err := ps.readSecretFile("/run/secrets/client_secret")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading CLIENT_SECRET: %v", err)
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"client_id": clientID,
|
||||
"client_secret": clientSecret,
|
||||
"scope": "azureopenai-readwrite",
|
||||
"grant_type": "client_credentials",
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshalling JSON: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := ps.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("request failed with status: %v", resp.Status)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("error decoding response: %v", err)
|
||||
}
|
||||
|
||||
token, ok := result["access_token"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("access token not found in response")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) handleProxy(w http.ResponseWriter, req *http.Request) {
|
||||
target := ps.azureEndpoint.ResolveReference(req.URL)
|
||||
// Create a proxy request
|
||||
proxyReq, err := http.NewRequest(req.Method, target.String(), req.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy headers from the original request
|
||||
for header, values := range req.Header {
|
||||
for _, value := range values {
|
||||
proxyReq.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
ps.tokenMutex.RLock()
|
||||
bearerToken := ps.accessToken
|
||||
ps.tokenMutex.RUnlock()
|
||||
// Add generated headers
|
||||
headers := ps.generateHeaders(bearerToken)
|
||||
for key, value := range headers {
|
||||
proxyReq.Header.Set(key, value)
|
||||
}
|
||||
proxyReq.Header.Set("Api-Key", bearerToken)
|
||||
|
||||
resp, err := ps.client.Do(proxyReq)
|
||||
if err != nil {
|
||||
http.Error(w, "Bad Gateway", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Write the headers and status code from the response to the client
|
||||
for header, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
w.Header().Add(header, value)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
// Stream the response body to the client
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
ps.logger.Error("Error reading response body: %v", log.Error(err))
|
||||
http.Error(w, "Error reading response from upstream server", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
if _, writeErr := w.Write(buf[:n]); writeErr != nil {
|
||||
ps.logger.Fatal("Error writing response: %v", log.Error(writeErr))
|
||||
break
|
||||
}
|
||||
if flusher, ok := w.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
liblog := log.Init(log.Resource{
|
||||
Name: "Special Oauth Server",
|
||||
})
|
||||
defer liblog.Sync()
|
||||
|
||||
logger := log.Scoped("server")
|
||||
|
||||
ps := &ProxyServer{
|
||||
logger: logger,
|
||||
}
|
||||
ps.initializeClient()
|
||||
ps.initializeAzureEndpoint()
|
||||
go ps.updateAccessToken()
|
||||
http.HandleFunc("/", ps.handleProxy)
|
||||
logger.Info("HTTP Proxy server is running on port 8080")
|
||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||
logger.Fatal("Failed to start HTTP server: %v", log.Error(err))
|
||||
}
|
||||
}
|
||||
62
cmd/customer-4512/BUILD.bazel
Normal file
62
cmd/customer-4512/BUILD.bazel
Normal file
@ -0,0 +1,62 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
load("@rules_pkg//:pkg.bzl", "pkg_tar")
|
||||
load("@container_structure_test//:defs.bzl", "container_structure_test")
|
||||
load("//dev:oci_defs.bzl", "image_repository", "oci_image", "oci_push", "oci_tarball")
|
||||
|
||||
go_library(
|
||||
name = "customer-4512_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/cmd/customer-4512",
|
||||
tags = [TAG_CODY_PRIME],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["@com_github_sourcegraph_log//:log"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "customer-4512",
|
||||
embed = [":customer-4512_lib"],
|
||||
tags = [TAG_CODY_PRIME],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "tar_customer-4512",
|
||||
srcs = [":customer-4512"],
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "image",
|
||||
base = "//wolfi-images/sourcegraph-base:base_image",
|
||||
entrypoint = [
|
||||
"/sbin/tini",
|
||||
"--",
|
||||
"/customer-4512",
|
||||
],
|
||||
tars = [":tar_customer-4512"],
|
||||
user = "sourcegraph",
|
||||
)
|
||||
|
||||
oci_tarball(
|
||||
name = "image_tarball",
|
||||
image = ":image",
|
||||
repo_tags = ["customer-4512:candidate"],
|
||||
)
|
||||
|
||||
container_structure_test(
|
||||
name = "image_test",
|
||||
timeout = "short",
|
||||
configs = ["image_test.yaml"],
|
||||
driver = "docker",
|
||||
image = ":image",
|
||||
tags = [
|
||||
"exclusive",
|
||||
"requires-network",
|
||||
TAG_CODY_PRIME,
|
||||
],
|
||||
)
|
||||
|
||||
oci_push(
|
||||
name = "candidate_push",
|
||||
image = ":image",
|
||||
repository = image_repository("customer-4512"),
|
||||
)
|
||||
15
cmd/customer-4512/image_test.yaml
Normal file
15
cmd/customer-4512/image_test.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
schemaVersion: "2.0.0"
|
||||
|
||||
commandTests:
|
||||
- name: "not running as root"
|
||||
command: "/usr/bin/id"
|
||||
args:
|
||||
- -u
|
||||
excludedOutput: ["^0"]
|
||||
exitCode: 0
|
||||
- name: "validate /customer-4512 file exists and is executable"
|
||||
command: "test"
|
||||
args:
|
||||
- "-x"
|
||||
- "/customer-4512"
|
||||
exitCode: 0
|
||||
204
cmd/customer-4512/main.go
Normal file
204
cmd/customer-4512/main.go
Normal file
@ -0,0 +1,204 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
accessToken string
|
||||
tokenMutex sync.RWMutex
|
||||
client *http.Client
|
||||
azureEndpoint *url.URL
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (ps *Proxy) readSecretFile(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(data)), nil
|
||||
}
|
||||
|
||||
func (ps *Proxy) updateAccessToken() {
|
||||
for {
|
||||
token, err := ps.getAccessToken()
|
||||
if err != nil {
|
||||
ps.logger.Fatal("Error getting access token: %v", log.Error(err))
|
||||
} else {
|
||||
ps.tokenMutex.Lock()
|
||||
ps.accessToken = token
|
||||
ps.tokenMutex.Unlock()
|
||||
ps.logger.Info("Access token updated")
|
||||
}
|
||||
time.Sleep(1 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *Proxy) initializeAzureEndpoint() {
|
||||
var err error
|
||||
azure_endpoint, err := ps.readSecretFile("/run/secrets/azure_endpoint")
|
||||
if err != nil {
|
||||
ps.logger.Fatal("error reading OAUTH_URL: %v", log.Error(err))
|
||||
}
|
||||
ps.azureEndpoint, err = url.Parse(azure_endpoint)
|
||||
if err != nil {
|
||||
ps.logger.Fatal("Invalid AZURE_ENDPOINT: %v", log.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *Proxy) initializeClient() {
|
||||
ps.client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 400,
|
||||
MaxIdleConnsPerHost: 400,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
},
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *Proxy) getAccessToken() (string, error) {
|
||||
oauth_url, err := ps.readSecretFile("/run/secrets/oauth_url")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading OAUTH_URL: %v", err)
|
||||
}
|
||||
clientID, err := ps.readSecretFile("/run/secrets/client_id")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading CLIENT_ID: %v", err)
|
||||
}
|
||||
clientSecret, err := ps.readSecretFile("/run/secrets/client_secret")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading CLIENT_SECRET: %v", err)
|
||||
}
|
||||
|
||||
authKey := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", clientID, clientSecret)))
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "client_credentials")
|
||||
|
||||
req, err := http.NewRequest("POST", oauth_url, io.NopCloser(strings.NewReader(data.Encode())))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Basic "+authKey)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := ps.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to retrieve token: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("Failed to retrieve token: %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to read response body: %v", err)
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
ps.logger.Fatal("Failed to unmarshal response body: %v", log.Error(err))
|
||||
}
|
||||
|
||||
accessToken, ok := result["access_token"].(string)
|
||||
if !ok {
|
||||
ps.logger.Fatal("Failed to retrieve access token from response body")
|
||||
}
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
func (ps *Proxy) handleProxy(w http.ResponseWriter, req *http.Request) {
|
||||
target := ps.azureEndpoint.ResolveReference(req.URL)
|
||||
// Create a proxy request
|
||||
proxyReq, err := http.NewRequest(req.Method, target.String(), req.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy headers from the original request
|
||||
for header, values := range req.Header {
|
||||
for _, value := range values {
|
||||
proxyReq.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
ps.tokenMutex.RLock()
|
||||
bearerToken := ps.accessToken
|
||||
ps.tokenMutex.RUnlock()
|
||||
|
||||
// Add accesstoken headers
|
||||
proxyReq.Header.Set("Api-Key", bearerToken)
|
||||
resp, err := ps.client.Do(proxyReq)
|
||||
if err != nil {
|
||||
http.Error(w, "Bad Gateway", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Write the headers and status code from the response to the client
|
||||
for header, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
w.Header().Add(header, value)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
// Stream the response body to the client
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
ps.logger.Error("Error reading response body: %v", log.Error(err))
|
||||
http.Error(w, "Error reading response from upstream server", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
if _, writeErr := w.Write(buf[:n]); writeErr != nil {
|
||||
ps.logger.Fatal("Error writing response: %v", log.Error(writeErr))
|
||||
break
|
||||
}
|
||||
if flusher, ok := w.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
liblog := log.Init(log.Resource{
|
||||
Name: "Cody OAuth Proxy",
|
||||
})
|
||||
defer liblog.Sync()
|
||||
|
||||
logger := log.Scoped("server")
|
||||
|
||||
ps := &Proxy{logger: logger}
|
||||
ps.initializeClient()
|
||||
ps.initializeAzureEndpoint()
|
||||
go ps.updateAccessToken()
|
||||
http.HandleFunc("/", ps.handleProxy)
|
||||
logger.Info("HTTP Proxy server is running on port 8080")
|
||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||
logger.Fatal("Failed to start HTTP server: %v", log.Error(err))
|
||||
}
|
||||
}
|
||||
@ -260,38 +260,41 @@ func GetAndSaveUser(
|
||||
// We handle all V2 telemetry related to GetAndSaveUser within this defer
|
||||
// closure, to ensure we cover all exit paths correctly after the other mega
|
||||
// closure above.
|
||||
defer func() {
|
||||
action := telemetry.ActionSucceeded
|
||||
if err != nil { // check final error
|
||||
action = telemetry.ActionFailed
|
||||
}
|
||||
//
|
||||
// We only store the event if a new user was created.
|
||||
if newUserSaved {
|
||||
defer func() {
|
||||
action := telemetry.ActionSucceeded
|
||||
if err != nil { // check final error
|
||||
action = telemetry.ActionFailed
|
||||
}
|
||||
|
||||
// Most auth providers services have an exstvc.Variant, so try and
|
||||
// extract that from the account spec. For ease of use in we also
|
||||
// preserve the raw value in the private metadata.
|
||||
serviceVariant, _ := extsvc.VariantValueOf(acct.AccountSpec.ServiceType)
|
||||
privateMetadata := map[string]any{"serviceType": acct.AccountSpec.ServiceType}
|
||||
// Most auth providers services have an exstvc.Variant, so try and
|
||||
// extract that from the account spec. For ease of use in we also
|
||||
// preserve the raw value in the private metadata.
|
||||
serviceVariant, _ := extsvc.VariantValueOf(acct.AccountSpec.ServiceType)
|
||||
privateMetadata := map[string]any{"serviceType": acct.AccountSpec.ServiceType}
|
||||
|
||||
// Include safe err if there is one for maybe-useful diagnostics
|
||||
if len(safeErrMsg) > 0 {
|
||||
privateMetadata["safeErrMsg"] = safeErrMsg
|
||||
}
|
||||
// Include safe err if there is one for maybe-useful diagnostics
|
||||
if len(safeErrMsg) > 0 {
|
||||
privateMetadata["safeErrMsg"] = safeErrMsg
|
||||
}
|
||||
|
||||
// Record our V2 event.
|
||||
recorder.Record(telemetryCtx, telemetryV2UserSignUpFeatureName, action, &telemetry.EventParameters{
|
||||
Version: 2, // We've significantly refactored telemetryV2UserSignUpFeatureName occurrences
|
||||
Metadata: telemetry.MergeMetadata(
|
||||
telemetry.EventMetadata{
|
||||
"serviceVariant": telemetry.Number(serviceVariant),
|
||||
// Track the various outcomes of the massive signup closure above.
|
||||
"newUserSaved": telemetry.Bool(newUserSaved),
|
||||
"extAcctSaved": telemetry.Bool(extAcctSaved),
|
||||
},
|
||||
op.UserCreateEventProperties,
|
||||
),
|
||||
PrivateMetadata: privateMetadata,
|
||||
})
|
||||
}()
|
||||
// Record our V2 event.
|
||||
recorder.Record(telemetryCtx, telemetryV2UserSignUpFeatureName, action, &telemetry.EventParameters{
|
||||
Version: 2, // We've significantly refactored telemetryV2UserSignUpFeatureName occurrences
|
||||
Metadata: telemetry.MergeMetadata(
|
||||
telemetry.EventMetadata{
|
||||
"serviceVariant": telemetry.Number(serviceVariant),
|
||||
// Track the various outcomes of the massive signup closure above.
|
||||
"extAcctSaved": telemetry.Bool(extAcctSaved),
|
||||
},
|
||||
op.UserCreateEventProperties,
|
||||
),
|
||||
PrivateMetadata: privateMetadata,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Legacy event - retain because it is still exported by the legacy
|
||||
|
||||
@ -507,9 +507,13 @@ func TestGetAndSaveUser(t *testing.T) {
|
||||
|
||||
// All telemetry should have the expected user (or lack
|
||||
// of user) attached, and all code paths should generate
|
||||
// at least 1 user event.
|
||||
// at least 1 user event if a new user was created.
|
||||
gotEvents := eventsStore.CollectStoredEvents()
|
||||
assert.NotEmpty(t, gotEvents)
|
||||
if c.expNewUserCreated {
|
||||
assert.NotEmpty(t, gotEvents)
|
||||
} else {
|
||||
assert.Empty(t, gotEvents)
|
||||
}
|
||||
for _, ev := range gotEvents {
|
||||
switch {
|
||||
// We are expecting a specific user ID
|
||||
|
||||
@ -232,13 +232,16 @@ type JSContext struct {
|
||||
CodeIntelAutoIndexingAllowGlobalPolicies bool `json:"codeIntelAutoIndexingAllowGlobalPolicies"`
|
||||
CodeIntelRankingDocumentReferenceCountsEnabled bool `json:"codeIntelRankingDocumentReferenceCountsEnabled"`
|
||||
|
||||
CodeInsightsEnabled bool `json:"codeInsightsEnabled"`
|
||||
CodeIntelligenceEnabled bool `json:"codeIntelligenceEnabled"`
|
||||
SearchContextsEnabled bool `json:"searchContextsEnabled"`
|
||||
NotebooksEnabled bool `json:"notebooksEnabled"`
|
||||
CodeMonitoringEnabled bool `json:"codeMonitoringEnabled"`
|
||||
SearchAggregationEnabled bool `json:"searchAggregationEnabled"`
|
||||
OwnEnabled bool `json:"ownEnabled"`
|
||||
CodeInsightsEnabled bool `json:"codeInsightsEnabled"`
|
||||
ApplianceUpdateTarget string `json:"applianceUpdateTarget"`
|
||||
ApplianceMenuTarget string `json:"applianceMenuTarget"`
|
||||
CodeIntelligenceEnabled bool `json:"codeIntelligenceEnabled"`
|
||||
SearchContextsEnabled bool `json:"searchContextsEnabled"`
|
||||
NotebooksEnabled bool `json:"notebooksEnabled"`
|
||||
CodeMonitoringEnabled bool `json:"codeMonitoringEnabled"`
|
||||
SearchAggregationEnabled bool `json:"searchAggregationEnabled"`
|
||||
OwnEnabled bool `json:"ownEnabled"`
|
||||
SearchJobsEnabled bool `json:"searchJobsEnabled"`
|
||||
|
||||
RedirectUnsupportedBrowser bool `json:"RedirectUnsupportedBrowser"`
|
||||
|
||||
@ -436,6 +439,8 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext {
|
||||
CodyRequiresVerifiedEmail: siteResolver.RequiresVerifiedEmailForCody(ctx),
|
||||
|
||||
CodeSearchEnabledOnInstance: codeSearchLicensed,
|
||||
ApplianceUpdateTarget: conf.ApplianceUpdateTarget(),
|
||||
ApplianceMenuTarget: conf.ApplianceMenuTarget(),
|
||||
|
||||
ExecutorsEnabled: conf.ExecutorsEnabled(),
|
||||
CodeIntelAutoIndexingEnabled: conf.CodeIntelAutoIndexingEnabled(),
|
||||
|
||||
@ -351,23 +351,18 @@ func addLimitsAndFilter(plan *search.Inputs, filter fileMatcher, args GetContext
|
||||
}
|
||||
|
||||
func fileMatchToContextMatch(fm *result.FileMatch) FileChunkContext {
|
||||
if len(fm.ChunkMatches) == 0 {
|
||||
var startLine int
|
||||
if len(fm.Symbols) != 0 {
|
||||
startLine = max(0, fm.Symbols[0].Symbol.Line-5) // 5 lines of leading context, clamped to zero
|
||||
} else if len(fm.ChunkMatches) != 0 {
|
||||
// To provide some context variety, we just use the top-ranked
|
||||
// chunk (the first chunk) from each file match.
|
||||
startLine = max(0, fm.ChunkMatches[0].ContentStart.Line-5) // 5 lines of leading context, clamped to zero
|
||||
} else {
|
||||
// If this is a filename-only match, return a single chunk at the start of the file
|
||||
return FileChunkContext{
|
||||
RepoName: fm.Repo.Name,
|
||||
RepoID: fm.Repo.ID,
|
||||
CommitID: fm.CommitID,
|
||||
Path: fm.Path,
|
||||
StartLine: 0,
|
||||
}
|
||||
startLine = 0
|
||||
}
|
||||
|
||||
// To provide some context variety, we just use the top-ranked
|
||||
// chunk (the first chunk) from each file
|
||||
|
||||
// 5 lines of leading context, clamped to zero
|
||||
startLine := max(0, fm.ChunkMatches[0].ContentStart.Line-5)
|
||||
|
||||
return FileChunkContext{
|
||||
RepoName: fm.Repo.Name,
|
||||
RepoID: fm.Repo.ID,
|
||||
|
||||
@ -64,6 +64,40 @@ func TestFileMatchToContextMatches(t *testing.T) {
|
||||
StartLine: 85,
|
||||
},
|
||||
},
|
||||
{
|
||||
// With symbol match returns context around first symbol
|
||||
fileMatch: &result.FileMatch{
|
||||
File: result.File{
|
||||
Path: "main.go",
|
||||
CommitID: "abc123",
|
||||
Repo: types.MinimalRepo{
|
||||
Name: "repo",
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
Symbols: []*result.SymbolMatch{
|
||||
{
|
||||
Symbol: result.Symbol{
|
||||
Line: 23,
|
||||
Name: "symbol",
|
||||
},
|
||||
},
|
||||
{
|
||||
Symbol: result.Symbol{
|
||||
Line: 37,
|
||||
Name: "symbol",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: FileChunkContext{
|
||||
RepoName: "repo",
|
||||
RepoID: 1,
|
||||
CommitID: "abc123",
|
||||
Path: "main.go",
|
||||
StartLine: 18,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
213
deps.bzl
213
deps.bzl
@ -34,8 +34,8 @@ def go_dependencies():
|
||||
name = "co_honnef_go_tools",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "honnef.co/go/tools",
|
||||
sum = "h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=",
|
||||
version = "v0.0.1-2020.1.4",
|
||||
sum = "h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o=",
|
||||
version = "v0.1.3",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_connectrpc_connect",
|
||||
@ -93,6 +93,20 @@ def go_dependencies():
|
||||
sum = "h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=",
|
||||
version = "v1.1.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_ajstarks_deck",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/ajstarks/deck",
|
||||
sum = "h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM=",
|
||||
version = "v0.0.0-20200831202436-30c9fc6549a9",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_ajstarks_deck_generate",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/ajstarks/deck/generate",
|
||||
sum = "h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8=",
|
||||
version = "v0.0.0-20210309230005-c3f852c02e19",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_ajstarks_svgo",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -275,6 +289,20 @@ def go_dependencies():
|
||||
sum = "h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=",
|
||||
version = "v1.0.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_apache_arrow_go_v10",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/apache/arrow/go/v10",
|
||||
sum = "h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI=",
|
||||
version = "v10.0.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_apache_arrow_go_v11",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/apache/arrow/go/v11",
|
||||
sum = "h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM=",
|
||||
version = "v11.0.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_apache_arrow_go_v14",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -793,6 +821,13 @@ def go_dependencies():
|
||||
sum = "h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=",
|
||||
version = "v0.0.0-20180917114910-cd5dcc76aeff",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_boombuler_barcode",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/boombuler/barcode",
|
||||
sum = "h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=",
|
||||
version = "v1.0.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_bradfitz_gomemcache",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -1087,8 +1122,8 @@ def go_dependencies():
|
||||
name = "com_github_cncf_udpa_go",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/cncf/udpa/go",
|
||||
sum = "h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI=",
|
||||
version = "v0.0.0-20210930031921-04548b0d99d4",
|
||||
sum = "h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk=",
|
||||
version = "v0.0.0-20220112060539-c52dc94e7fbe",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_cncf_xds_go",
|
||||
@ -1902,6 +1937,13 @@ def go_dependencies():
|
||||
sum = "h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=",
|
||||
version = "v4.0.2",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_fogleman_gg",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/fogleman/gg",
|
||||
sum = "h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=",
|
||||
version = "v1.3.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_form3tech_oss_jwt_go",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -2043,6 +2085,13 @@ def go_dependencies():
|
||||
sum = "h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=",
|
||||
version = "v0.6.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_go_fonts_dejavu",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/go-fonts/dejavu",
|
||||
sum = "h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ=",
|
||||
version = "v0.1.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_go_fonts_latin_modern",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -2057,6 +2106,13 @@ def go_dependencies():
|
||||
sum = "h1:XuwG0vGHFBPRRI8Qwbi5tIvR3cku9LUfZGq/Ar16wlQ=",
|
||||
version = "v0.3.2",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_go_fonts_stix",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/go-fonts/stix",
|
||||
sum = "h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg=",
|
||||
version = "v0.1.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_go_git_gcfg",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -2782,6 +2838,13 @@ def go_dependencies():
|
||||
sum = "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
|
||||
version = "v0.5.5",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_googleapis_go_type_adapters",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/googleapis/go-type-adapters",
|
||||
sum = "h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=",
|
||||
version = "v1.0.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_googleapis_google_cloud_go_testing",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -3920,8 +3983,8 @@ def go_dependencies():
|
||||
name = "com_github_konsorten_go_windows_terminal_sequences",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/konsorten/go-windows-terminal-sequences",
|
||||
sum = "h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=",
|
||||
version = "v1.0.3",
|
||||
sum = "h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=",
|
||||
version = "v1.0.2",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_kr_fs",
|
||||
@ -4098,6 +4161,13 @@ def go_dependencies():
|
||||
sum = "h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=",
|
||||
version = "v0.0.0-20211012122336-39d0f177ccd0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_lyft_protoc_gen_star",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/lyft/protoc-gen-star",
|
||||
sum = "h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M=",
|
||||
version = "v0.6.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_lyft_protoc_gen_star_v2",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -4935,6 +5005,20 @@ def go_dependencies():
|
||||
sum = "h1:s2+RH8EGuI/mI4QwrWGSYQCRz7uNgip9BaM04HKu5kc=",
|
||||
version = "v1.1.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_phpdave11_gofpdf",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/phpdave11/gofpdf",
|
||||
sum = "h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ=",
|
||||
version = "v1.4.2",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_phpdave11_gofpdi",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/phpdave11/gofpdi",
|
||||
sum = "h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ=",
|
||||
version = "v1.0.13",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_pierrec_lz4",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -5087,8 +5171,9 @@ def go_dependencies():
|
||||
name = "com_github_prometheus_common",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/prometheus/common",
|
||||
sum = "h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=",
|
||||
version = "v0.54.0",
|
||||
replace = "github.com/prometheus/common",
|
||||
sum = "h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=",
|
||||
version = "v0.48.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_prometheus_common_assets",
|
||||
@ -5381,6 +5466,13 @@ def go_dependencies():
|
||||
sum = "h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=",
|
||||
version = "v2.1.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_ruudk_golang_pdf417",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/ruudk/golang-pdf417",
|
||||
sum = "h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY=",
|
||||
version = "v0.0.0-20201230142125-a7e3863a1245",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_ryanuber_columnize",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -6327,13 +6419,6 @@ def go_dependencies():
|
||||
sum = "h1:dH55ru2OQOIAKjZi5wwXjNnSfN0oXLFYkMQy908s+tU=",
|
||||
version = "v0.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_wagslane_go_password_validator",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/wagslane/go-password-validator",
|
||||
sum = "h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=",
|
||||
version = "v0.3.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_wk8_go_ordered_map_v2",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -6621,6 +6706,13 @@ def go_dependencies():
|
||||
sum = "h1:l8VFHdNMC+9Q4EHKye2eOZBu5IwddXF6ufAXI7D+PB8=",
|
||||
version = "v0.8.4",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_apikeys",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/apikeys",
|
||||
sum = "h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ=",
|
||||
version = "v0.6.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_appengine",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -6831,6 +6923,13 @@ def go_dependencies():
|
||||
sum = "h1:Ob8NPT1UcB4kDaDx7/UdsRfZ8xUvUggZshXUlGWDahk=",
|
||||
version = "v1.15.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_dataproc",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/dataproc",
|
||||
sum = "h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU=",
|
||||
version = "v1.12.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_dataproc_v2",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -6943,6 +7042,13 @@ def go_dependencies():
|
||||
sum = "h1:0kcko/2AKwm4USnWcGs/W/k++PAYPA3dYaQw1y5Xg3M=",
|
||||
version = "v1.16.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_gaming",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/gaming",
|
||||
sum = "h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc=",
|
||||
version = "v1.9.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_gkebackup",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -6971,6 +7077,13 @@ def go_dependencies():
|
||||
sum = "h1:CFBoDcQi9zLOkzM6xqmRzljZhF4A6A47QaQ0WtNd+DA=",
|
||||
version = "v1.1.2",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_grafeas",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/grafeas",
|
||||
sum = "h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM=",
|
||||
version = "v0.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_gsuiteaddons",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -7188,6 +7301,13 @@ def go_dependencies():
|
||||
sum = "h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY=",
|
||||
version = "v1.8.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_recaptchaenterprise",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/recaptchaenterprise",
|
||||
sum = "h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ=",
|
||||
version = "v1.3.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_recaptchaenterprise_v2",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -7272,6 +7392,13 @@ def go_dependencies():
|
||||
sum = "h1:NpEJeFbm3ad3ibpbpIBKXJS7eQq1cZhtt9nrDTMO/QQ=",
|
||||
version = "v1.28.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_servicecontrol",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/servicecontrol",
|
||||
sum = "h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE=",
|
||||
version = "v1.11.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_servicedirectory",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -7279,6 +7406,20 @@ def go_dependencies():
|
||||
sum = "h1:gkzx9Cd+OTOD+zY4u5vtbdvOx7vrvHYdeDiNdC6vKyw=",
|
||||
version = "v1.11.5",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_servicemanagement",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/servicemanagement",
|
||||
sum = "h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY=",
|
||||
version = "v1.8.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_serviceusage",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/serviceusage",
|
||||
sum = "h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4=",
|
||||
version = "v1.6.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_shell",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -7363,6 +7504,13 @@ def go_dependencies():
|
||||
sum = "h1:P0Sa8+5KOEAVk/fazUNjVPzRCijCheZWJ8wL8xBn9Uk=",
|
||||
version = "v1.11.6",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_vision",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "cloud.google.com/go/vision",
|
||||
sum = "h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4=",
|
||||
version = "v1.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_google_cloud_go_vision_v2",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -8548,6 +8696,13 @@ def go_dependencies():
|
||||
sum = "h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=",
|
||||
version = "v0.15.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_gonum_v1_netlib",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "gonum.org/v1/netlib",
|
||||
sum = "h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=",
|
||||
version = "v0.0.0-20190313105609-8cb42192e0e0",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_gonum_v1_plot",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -8597,6 +8752,13 @@ def go_dependencies():
|
||||
sum = "h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo=",
|
||||
version = "v4.17.10",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_modernc_ccorpus",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "modernc.org/ccorpus",
|
||||
sum = "h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=",
|
||||
version = "v1.11.6",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_modernc_fileutil",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -8618,6 +8780,13 @@ def go_dependencies():
|
||||
sum = "h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=",
|
||||
version = "v3.0.0-20240107210532-573471604cb6",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_modernc_httpfs",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "modernc.org/httpfs",
|
||||
sum = "h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=",
|
||||
version = "v1.0.6",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_modernc_libc",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -8667,6 +8836,13 @@ def go_dependencies():
|
||||
sum = "h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=",
|
||||
version = "v1.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_modernc_tcl",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "modernc.org/tcl",
|
||||
sum = "h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao=",
|
||||
version = "v1.13.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_modernc_token",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -8674,6 +8850,13 @@ def go_dependencies():
|
||||
sum = "h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=",
|
||||
version = "v1.1.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_modernc_z",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "modernc.org/z",
|
||||
sum = "h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=",
|
||||
version = "v1.5.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "org_mongodb_go_mongo_driver",
|
||||
build_file_proto_mode = "disable_global",
|
||||
|
||||
@ -127,5 +127,6 @@ write_source_files(
|
||||
"//cmd/enterprise-portal/internal/subscriptionsservice:generate_mocks",
|
||||
"//dev/sg/internal/analytics:generate_mocks",
|
||||
"//cmd/symbols/internal/fetcher:generate_mocks",
|
||||
"//internal/releaseregistry/mocks:generate_mocks",
|
||||
],
|
||||
)
|
||||
|
||||
@ -275,6 +275,8 @@ func GeneratePipeline(c Config) (*bk.Pipeline, error) {
|
||||
)
|
||||
case runtype.PromoteRelease:
|
||||
ops = operations.NewSet(
|
||||
checkSecurityApproval(c),
|
||||
wait,
|
||||
releasePromoteImages(c),
|
||||
wait,
|
||||
releaseTestOperation(c),
|
||||
|
||||
@ -11,6 +11,25 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/dev/ci/internal/ci/operations"
|
||||
)
|
||||
|
||||
// checkSecurityApproval checks whether the specified release has release approval from the Security Team.
|
||||
func checkSecurityApproval(c Config) operations.Operation {
|
||||
return func(pipeline *bk.Pipeline) {
|
||||
pipeline.AddStep(":nodesecurity: Check security approval",
|
||||
bk.Agent("queue", AspectWorkflows.QueueDefault),
|
||||
bk.Env("VERSION", c.Version),
|
||||
bk.AnnotatedCmd(
|
||||
"./tools/release/check_security_approval.sh",
|
||||
bk.AnnotatedCmdOpts{
|
||||
Annotations: &bk.AnnotationOpts{
|
||||
Type: bk.AnnotationTypeInfo,
|
||||
IncludeNames: false,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// releasePromoteImages runs a script that iterates through all defined images that we're producing that has been uploaded
|
||||
// on the internal registry with a given version and retags them to the public registry.
|
||||
func releasePromoteImages(c Config) operations.Operation {
|
||||
|
||||
@ -119,7 +119,7 @@ elif [[ "$BUILDKITE_BRANCH" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
# format introduced by https://github.com/sourcegraph/sourcegraph/pull/48050
|
||||
# by release branch deployments.
|
||||
push_prod=true
|
||||
elif [[ "$BUILDKITE_BRANCH" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
elif [[ "$BUILDKITE_BRANCH" =~ ^[0-9]+\.[0-9]+\.(x|[0-9]+)$ ]]; then
|
||||
# Patch release builds only need to be pushed to internal registries.
|
||||
push_prod=false
|
||||
dev_tags+=("$BUILDKITE_BRANCH-insiders")
|
||||
|
||||
@ -22,6 +22,8 @@ const (
|
||||
WolfiBaseRebuild // wolfi base image build
|
||||
|
||||
// Release branches
|
||||
InternalRelease // Internal release
|
||||
PromoteRelease // Public release
|
||||
|
||||
TaggedRelease // semver-tagged release
|
||||
ReleaseBranch // release branch build
|
||||
@ -29,9 +31,6 @@ const (
|
||||
BextReleaseBranch // browser extension release build
|
||||
VsceReleaseBranch // vs code extension release build
|
||||
|
||||
InternalRelease // Internal release
|
||||
PromoteRelease // Public release
|
||||
|
||||
// Main branches
|
||||
|
||||
MainBranch // main branch build
|
||||
@ -141,7 +140,7 @@ func (t RunType) Matcher() *RunTypeMatcher {
|
||||
}
|
||||
case PatchReleaseBranch:
|
||||
return &RunTypeMatcher{
|
||||
Branch: `^[0-9]+\.[0-9]+\.[0-9]+$`,
|
||||
Branch: `^[0-9]+\.[0-9]+\.(?:x|[0-9]+)$`,
|
||||
BranchRegexp: true,
|
||||
}
|
||||
case BextReleaseBranch:
|
||||
|
||||
71
docker-images/appliance-frontend/BUILD.bazel
Normal file
71
docker-images/appliance-frontend/BUILD.bazel
Normal file
@ -0,0 +1,71 @@
|
||||
load("//dev:oci_defs.bzl", "image_repository", "oci_image", "oci_push", "oci_tarball")
|
||||
load("@rules_pkg//:pkg.bzl", "pkg_tar")
|
||||
load("@container_structure_test//:defs.bzl", "container_structure_test")
|
||||
load("//wolfi-images:defs.bzl", "wolfi_base")
|
||||
|
||||
filegroup(
|
||||
name = "config",
|
||||
srcs = ["nginx.conf"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "init_script",
|
||||
srcs = ["init.sh"],
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "tar_config",
|
||||
srcs = [":config"],
|
||||
package_dir = "/etc/nginx",
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "tar_init_script",
|
||||
srcs = [":init_script"],
|
||||
package_dir = "/",
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "image",
|
||||
base = ":base_image",
|
||||
entrypoint = [
|
||||
"/init.sh",
|
||||
"nginx",
|
||||
"-g",
|
||||
"daemon off;",
|
||||
],
|
||||
tars = [
|
||||
":tar_init_script",
|
||||
":tar_config",
|
||||
"//internal/appliance/frontend/maintenance:tar_config",
|
||||
"//internal/appliance/frontend/maintenance:tar_frontend",
|
||||
],
|
||||
user = "sourcegraph",
|
||||
)
|
||||
|
||||
oci_tarball(
|
||||
name = "image_tarball",
|
||||
image = ":image",
|
||||
repo_tags = ["appliance-frontend:candidate"],
|
||||
)
|
||||
|
||||
container_structure_test(
|
||||
name = "image_test",
|
||||
timeout = "short",
|
||||
configs = ["image_test.yaml"],
|
||||
driver = "docker",
|
||||
image = ":image",
|
||||
tags = [
|
||||
"exclusive",
|
||||
"requires-network",
|
||||
TAG_INFRA_DEVINFRA,
|
||||
],
|
||||
)
|
||||
|
||||
oci_push(
|
||||
name = "candidate_push",
|
||||
image = ":image",
|
||||
repository = image_repository("appliance-frontend"),
|
||||
)
|
||||
|
||||
wolfi_base()
|
||||
14
docker-images/appliance-frontend/image_test.yaml
Executable file
14
docker-images/appliance-frontend/image_test.yaml
Executable file
@ -0,0 +1,14 @@
|
||||
schemaVersion: "2.0.0"
|
||||
|
||||
commandTests:
|
||||
- name: "nginx is runnable"
|
||||
command: "nginx"
|
||||
args:
|
||||
- -v
|
||||
|
||||
- name: "not running as root"
|
||||
command: "/usr/bin/id"
|
||||
args:
|
||||
- -u
|
||||
excludedOutput: ["^0"]
|
||||
exitCode: 0
|
||||
18
docker-images/appliance-frontend/init.sh
Executable file
18
docker-images/appliance-frontend/init.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}"
|
||||
suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}"
|
||||
output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}"
|
||||
filter="${NGINX_ENVSUBST_FILTER:-}"
|
||||
# shellcheck disable=SC2046
|
||||
defined_envs=$(printf "\${%s} " $(awk "END { for (name in ENVIRON) { print ( name ~ /${filter}/ ) ? name : \"\" } }" </dev/null))
|
||||
|
||||
for template in /etc/nginx/templates/*.template; do
|
||||
relative_path="${template#"$template_dir/"}"
|
||||
output_path="$output_dir/${relative_path%"$suffix"}"
|
||||
subdir=$(dirname "$relative_path")
|
||||
mkdir -p "$output_dir/$subdir"
|
||||
echo "Processing $template -> $output_path"
|
||||
envsubst "$defined_envs" <"$template" >"$output_path"
|
||||
done
|
||||
|
||||
exec "$@"
|
||||
16
docker-images/appliance-frontend/nginx.conf
Executable file
16
docker-images/appliance-frontend/nginx.conf
Executable file
@ -0,0 +1,16 @@
|
||||
worker_processes 1;
|
||||
error_log stderr warn;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log off;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
include conf.d/*.conf;
|
||||
}
|
||||
10
go.mod
10
go.mod
@ -45,6 +45,11 @@ replace (
|
||||
github.com/googleapis/gnostic => github.com/googleapis/gnostic v0.5.5
|
||||
// Pending: https://github.com/openfga/openfga/pull/1688
|
||||
github.com/openfga/openfga => github.com/sourcegraph/openfga v0.0.0-20240614204729-de6b563022de
|
||||
// We need to wait for https://github.com/prometheus/alertmanager to cut a
|
||||
// release that uses a newer 'prometheus/common'. Then we need to update
|
||||
// https://github.com/sourcegraph/alertmanager. Upgrading before then will
|
||||
// cause problems with generated alertmanager configuration.
|
||||
github.com/prometheus/common => github.com/prometheus/common v0.48.0
|
||||
// Pending: https://github.com/shurcooL/httpgzip/pull/9
|
||||
github.com/shurcooL/httpgzip => github.com/sourcegraph/httpgzip v0.0.0-20211015085752-0bad89b3b4df
|
||||
)
|
||||
@ -253,6 +258,7 @@ require (
|
||||
connectrpc.com/connect v1.16.2
|
||||
connectrpc.com/grpcreflect v1.2.0
|
||||
connectrpc.com/otelconnect v0.7.0
|
||||
dario.cat/mergo v1.0.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.5.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
|
||||
@ -270,7 +276,6 @@ require (
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-redsync/redsync/v4 v4.13.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/go-containerregistry v0.16.1
|
||||
github.com/google/go-github/v48 v48.2.0
|
||||
github.com/google/go-github/v55 v55.0.0
|
||||
@ -315,7 +320,6 @@ require (
|
||||
github.com/sourcegraph/sourcegraph/monitoring v0.0.0-00010101000000-000000000000
|
||||
github.com/vektah/gqlparser/v2 v2.4.5
|
||||
github.com/vvakame/gcplogurl v0.2.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
go.opentelemetry.io/collector/config/confighttp v0.103.0
|
||||
go.opentelemetry.io/collector/config/configtelemetry v0.103.0
|
||||
go.opentelemetry.io/collector/config/configtls v0.103.0
|
||||
@ -337,7 +341,6 @@ require (
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.6 // indirect
|
||||
cloud.google.com/go/trace v1.10.6 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
|
||||
@ -385,6 +388,7 @@ require (
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.0.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/cel-go v0.20.1 // indirect
|
||||
|
||||
@ -6,24 +6,14 @@ go_library(
|
||||
srcs = [
|
||||
"appliance.go",
|
||||
"auth.go",
|
||||
"embed.go",
|
||||
"errors.go",
|
||||
"grpc.go",
|
||||
"html.go",
|
||||
"json.go",
|
||||
"routes.go",
|
||||
"status.go",
|
||||
"versions.go",
|
||||
],
|
||||
embedsrcs = [
|
||||
"web/static/img/favicon.png",
|
||||
"web/static/script/htmx.min.js",
|
||||
"web/template/setup.gohtml",
|
||||
"web/static/css/bootstrap.min.css",
|
||||
"web/static/css/custom.css",
|
||||
"web/static/script/bootstrap.bundle.min.js",
|
||||
"web/template/layout.gohtml",
|
||||
"web/template/landing.gohtml",
|
||||
"web/template/error.gohtml",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/internal/appliance",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
@ -32,12 +22,12 @@ go_library(
|
||||
"//internal/releaseregistry",
|
||||
"//lib/errors",
|
||||
"//lib/pointers",
|
||||
"@com_github_golang_jwt_jwt_v5//:jwt",
|
||||
"@cat_dario_mergo//:mergo",
|
||||
"@com_github_gorilla_mux//:mux",
|
||||
"@com_github_life4_genesis//slices",
|
||||
"@com_github_masterminds_semver_v3//:semver",
|
||||
"@com_github_sourcegraph_log//:log",
|
||||
"@com_github_wagslane_go_password_validator//:go-password-validator",
|
||||
"@io_k8s_api//apps/v1:apps",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apimachinery//pkg/api/errors",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||
@ -57,12 +47,17 @@ go_test(
|
||||
name = "appliance_test",
|
||||
srcs = [
|
||||
"auth_test.go",
|
||||
"json_test.go",
|
||||
"status_test.go",
|
||||
"versions_test.go",
|
||||
],
|
||||
embed = [":appliance"],
|
||||
deps = [
|
||||
"@com_github_golang_jwt_jwt_v5//:jwt",
|
||||
"@com_github_google_go_cmp//cmp",
|
||||
"@com_github_sourcegraph_log//:log",
|
||||
"@com_github_stretchr_testify//require",
|
||||
"@io_k8s_api//apps/v1:apps",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_sigs_controller_runtime//pkg/client",
|
||||
],
|
||||
)
|
||||
|
||||
@ -2,17 +2,18 @@ package appliance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/config"
|
||||
pb "github.com/sourcegraph/sourcegraph/internal/appliance/v1"
|
||||
"github.com/sourcegraph/sourcegraph/internal/releaseregistry"
|
||||
@ -21,15 +22,15 @@ import (
|
||||
)
|
||||
|
||||
type Appliance struct {
|
||||
jwtSecret []byte
|
||||
adminPasswordBcrypt []byte
|
||||
|
||||
client client.Client
|
||||
namespace string
|
||||
status Status
|
||||
status config.Status
|
||||
sourcegraph *config.Sourcegraph
|
||||
releaseRegistryClient *releaseregistry.Client
|
||||
latestSupportedVersion string
|
||||
noResourceRestrictions bool
|
||||
logger log.Logger
|
||||
|
||||
// Embed the UnimplementedApplianceServiceServer structs to ensure forwards compatibility (if the service is
|
||||
@ -38,31 +39,20 @@ type Appliance struct {
|
||||
pb.UnimplementedApplianceServiceServer
|
||||
}
|
||||
|
||||
// Status is a Stage that an Appliance can be in.
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusUnknown Status = "unknown"
|
||||
StatusSetup Status = "setup"
|
||||
StatusInstalling Status = "installing"
|
||||
|
||||
// Secret and key names
|
||||
dataSecretName = "appliance-data"
|
||||
dataSecretJWTSigningKeyKey = "jwt-signing-key"
|
||||
dataSecretEncryptedPasswordKey = "encrypted-admin-password"
|
||||
initialPasswordSecretName = "appliance-password"
|
||||
initialPasswordSecretPasswordKey = "password"
|
||||
)
|
||||
|
||||
func (s Status) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func NewAppliance(
|
||||
client client.Client,
|
||||
relregClient *releaseregistry.Client,
|
||||
latestSupportedVersion string,
|
||||
namespace string,
|
||||
noResourceRestrictions bool,
|
||||
logger log.Logger,
|
||||
) (*Appliance, error) {
|
||||
app := &Appliance{
|
||||
@ -70,7 +60,8 @@ func NewAppliance(
|
||||
releaseRegistryClient: relregClient,
|
||||
latestSupportedVersion: latestSupportedVersion,
|
||||
namespace: namespace,
|
||||
status: StatusSetup,
|
||||
status: config.StatusInstall,
|
||||
noResourceRestrictions: noResourceRestrictions,
|
||||
sourcegraph: &config.Sourcegraph{},
|
||||
logger: logger,
|
||||
}
|
||||
@ -109,13 +100,6 @@ func (a *Appliance) ensureBackingSecretKeysExist(ctx context.Context, secret *co
|
||||
if secret.Data == nil {
|
||||
secret.Data = map[string][]byte{}
|
||||
}
|
||||
if _, ok := secret.Data[dataSecretJWTSigningKeyKey]; !ok {
|
||||
jwtSigningKey, err := genRandomBytes(32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.Data[dataSecretJWTSigningKeyKey] = jwtSigningKey
|
||||
}
|
||||
|
||||
if _, ok := secret.Data[dataSecretEncryptedPasswordKey]; !ok {
|
||||
// Get admin-supplied password from separate secret, then delete it
|
||||
@ -150,88 +134,106 @@ func (a *Appliance) ensureBackingSecretKeysExist(ctx context.Context, secret *co
|
||||
}
|
||||
|
||||
func (a *Appliance) loadValuesFromSecret(secret *corev1.Secret) {
|
||||
a.jwtSecret = secret.Data[dataSecretJWTSigningKeyKey]
|
||||
a.adminPasswordBcrypt = secret.Data[dataSecretEncryptedPasswordKey]
|
||||
}
|
||||
|
||||
func genRandomBytes(length int) ([]byte, error) {
|
||||
randomBytes := make([]byte, length)
|
||||
bytesRead, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "reading random bytes")
|
||||
}
|
||||
if bytesRead != length {
|
||||
return nil, errors.Newf("expected to read %d random bytes, got %d", length, bytesRead)
|
||||
}
|
||||
return randomBytes, nil
|
||||
}
|
||||
|
||||
func (a *Appliance) GetCurrentVersion(ctx context.Context) string {
|
||||
return a.sourcegraph.Status.CurrentVersion
|
||||
}
|
||||
|
||||
func (a *Appliance) GetCurrentStatus(ctx context.Context) Status {
|
||||
func (a *Appliance) GetCurrentStatus(ctx context.Context) config.Status {
|
||||
return a.status
|
||||
}
|
||||
|
||||
func (a *Appliance) CreateConfigMap(ctx context.Context, name string) (*corev1.ConfigMap, error) {
|
||||
spec, err := yaml.Marshal(a.sourcegraph)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (a *Appliance) reconcileConfigMap(ctx context.Context, configMap *corev1.ConfigMap) error {
|
||||
existingCfgMapName := types.NamespacedName{Name: config.ConfigmapName, Namespace: a.namespace}
|
||||
existingCfgMap := &corev1.ConfigMap{}
|
||||
if err := a.client.Get(ctx, existingCfgMapName, existingCfgMap); err != nil {
|
||||
// Create the ConfigMap if not found
|
||||
if apierrors.IsNotFound(err) {
|
||||
spec, err := yaml.Marshal(a.sourcegraph)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal configmap yaml")
|
||||
}
|
||||
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: a.namespace,
|
||||
Labels: map[string]string{
|
||||
cfgMap := &corev1.ConfigMap{}
|
||||
cfgMap.Name = config.ConfigmapName
|
||||
cfgMap.Namespace = a.namespace
|
||||
|
||||
cfgMap.Labels = map[string]string{
|
||||
"deploy": "sourcegraph",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
}
|
||||
|
||||
cfgMap.Annotations = map[string]string{
|
||||
// required annotation for our controller filter.
|
||||
config.AnnotationKeyManaged: "true",
|
||||
},
|
||||
},
|
||||
Immutable: pointers.Ptr(false),
|
||||
Data: map[string]string{
|
||||
"spec": string(spec),
|
||||
},
|
||||
config.AnnotationKeyStatus: string(config.StatusUnknown),
|
||||
config.AnnotationConditions: "",
|
||||
}
|
||||
|
||||
if configMap.ObjectMeta.Annotations != nil {
|
||||
cfgMap.ObjectMeta.Annotations = configMap.ObjectMeta.Annotations
|
||||
}
|
||||
|
||||
cfgMap.Immutable = pointers.Ptr(false)
|
||||
cfgMap.Data = map[string]string{"spec": string(spec)}
|
||||
|
||||
return a.client.Create(ctx, cfgMap)
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "getting configmap")
|
||||
}
|
||||
|
||||
if err := a.client.Create(ctx, configMap); err != nil {
|
||||
return nil, err
|
||||
// The configmap already exists, update with any changed values
|
||||
if err := mergo.Merge(existingCfgMap, configMap, mergo.WithOverride); err != nil {
|
||||
return errors.Wrap(err, "merging configmaps")
|
||||
}
|
||||
|
||||
return configMap, nil
|
||||
return a.client.Update(ctx, existingCfgMap)
|
||||
}
|
||||
|
||||
func (a *Appliance) GetConfigMap(ctx context.Context, name string) (*corev1.ConfigMap, error) {
|
||||
var applianceSpec corev1.ConfigMap
|
||||
err := a.client.Get(ctx, types.NamespacedName{Name: name, Namespace: a.namespace}, &applianceSpec)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
// isSourcegraphFrontendReady is a "health check" that is used to be able to know when our backing sourcegraph
|
||||
// deployment is ready. This is a "quick and dirty" function and should be replaced with a more comprehensive
|
||||
// health check in the very near future.
|
||||
func (a *Appliance) isSourcegraphFrontendReady(ctx context.Context) (bool, error) {
|
||||
frontendDeploymentName := types.NamespacedName{Name: "sourcegraph-frontend", Namespace: a.namespace}
|
||||
frontendDeployment := &appsv1.Deployment{}
|
||||
if err := a.client.Get(ctx, frontendDeploymentName, frontendDeployment); err != nil {
|
||||
// If the frontend deployment is not found, we can assume it's not ready
|
||||
if apierrors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.Wrap(err, "fetching frontend deployment")
|
||||
}
|
||||
|
||||
return &applianceSpec, nil
|
||||
return IsObjectReady(frontendDeployment)
|
||||
}
|
||||
|
||||
func (a *Appliance) shouldSetupRun(ctx context.Context) (bool, error) {
|
||||
cfgMap, err := a.GetConfigMap(ctx, "sourcegraph-appliance")
|
||||
switch {
|
||||
case err != nil:
|
||||
return false, err
|
||||
case a.status == StatusInstalling:
|
||||
// configMap does not exist but is being created
|
||||
return false, nil
|
||||
case cfgMap == nil:
|
||||
// configMap does not exist
|
||||
return true, nil
|
||||
case cfgMap.Annotations[config.AnnotationKeyManaged] == "false":
|
||||
// appliance is not managed
|
||||
return false, nil
|
||||
default:
|
||||
return true, nil
|
||||
func (a *Appliance) getStatus(ctx context.Context) (config.Status, error) {
|
||||
configMapName := types.NamespacedName{Name: config.ConfigmapName, Namespace: a.namespace}
|
||||
configMap := &corev1.ConfigMap{}
|
||||
if err := a.client.Get(ctx, configMapName, configMap); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return config.StatusUnknown, nil
|
||||
}
|
||||
return config.StatusUnknown, err
|
||||
}
|
||||
|
||||
return config.Status(configMap.ObjectMeta.Annotations[config.AnnotationKeyStatus]), nil
|
||||
}
|
||||
|
||||
func (a *Appliance) setStatus(ctx context.Context, status config.Status) error {
|
||||
configMapName := types.NamespacedName{Name: config.ConfigmapName, Namespace: a.namespace}
|
||||
configMap := &corev1.ConfigMap{}
|
||||
if err := a.client.Get(ctx, configMapName, configMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configMap.Annotations[config.AnnotationKeyStatus] = string(status)
|
||||
err := a.client.Update(ctx, configMap)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed set status")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -2,74 +2,34 @@ package appliance
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
"sync"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
authCookieName = "applianceAuth"
|
||||
jwtClaimsValidUntilKey = "valid-until"
|
||||
authHeaderName = "admin-password"
|
||||
)
|
||||
|
||||
func (a *Appliance) CheckAuthorization(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
authCookie, err := req.Cookie(authCookieName)
|
||||
if err != nil {
|
||||
a.authRedirect(w, req, err)
|
||||
// The bcrypt operation is expensive, and the frontend calls auth-gated
|
||||
// endpoints in a tight loop. Caching valid passwords in memory massively
|
||||
// improves performance.
|
||||
var authzCache = &sync.Map{}
|
||||
|
||||
func (a *Appliance) checkAuthorization(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userPass := r.Header.Get(authHeaderName)
|
||||
if _, ok := authzCache.Load(userPass); ok {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(authCookie.Value, func(token *jwt.Token) (any, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return a.jwtSecret, nil
|
||||
})
|
||||
if err != nil {
|
||||
a.authRedirect(w, req, err)
|
||||
return
|
||||
}
|
||||
if !token.Valid {
|
||||
a.authRedirect(w, req, errors.New("JWT is not valid"))
|
||||
if err := bcrypt.CompareHashAndPassword(a.adminPasswordBcrypt, []byte(userPass)); err != nil {
|
||||
a.invalidAdminPasswordResponse(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
a.authRedirect(w, req, errors.New("JWT Claims are not a MapClaims"))
|
||||
return
|
||||
}
|
||||
validUntilStr, ok := claims[jwtClaimsValidUntilKey].(string)
|
||||
if !ok {
|
||||
err := errors.Newf("JWT does not contain a string field '%s'", jwtClaimsValidUntilKey)
|
||||
a.authRedirect(w, req, err)
|
||||
return
|
||||
}
|
||||
validUntil, err := time.Parse(time.RFC3339, validUntilStr)
|
||||
if err != nil {
|
||||
a.authRedirect(w, req, errors.Wrapf(err, "parsing %s field on JWT claims", jwtClaimsValidUntilKey))
|
||||
return
|
||||
}
|
||||
if time.Now().After(validUntil) {
|
||||
a.authRedirect(w, req, errors.Newf("JWT expired: %s", validUntil.String()))
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
authzCache.Store(userPass, struct{}{})
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Appliance) authRedirect(w http.ResponseWriter, req *http.Request, err error) {
|
||||
a.logger.Info("admin authorization failed", log.Error(err))
|
||||
deletedCookie := &http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: "",
|
||||
Expires: time.Unix(0, 0),
|
||||
}
|
||||
http.SetCookie(w, deletedCookie)
|
||||
http.Redirect(w, req, "/appliance/login", http.StatusFound)
|
||||
}
|
||||
|
||||
@ -4,122 +4,64 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
)
|
||||
|
||||
var appliance = &Appliance{
|
||||
jwtSecret: []byte("a-jwt-secret"),
|
||||
logger: log.NoOp(),
|
||||
}
|
||||
|
||||
func TestCheckAuthorization_CallsNextHandlerWhenValidJWTSupplied(t *testing.T) {
|
||||
validUntil := time.Now().Add(time.Hour).UTC()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
jwtClaimsValidUntilKey: validUntil.Format(time.RFC3339),
|
||||
})
|
||||
tokenStr, err := token.SignedString(appliance.jwtSecret)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("GET", "example.com", nil)
|
||||
require.NoError(t, err)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: tokenStr,
|
||||
Expires: validUntil,
|
||||
})
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
})
|
||||
respSpy := httptest.NewRecorder()
|
||||
appliance.CheckAuthorization(handler).ServeHTTP(respSpy, req)
|
||||
|
||||
require.Equal(t, http.StatusAccepted, respSpy.Code)
|
||||
}
|
||||
|
||||
func TestCheckAuthorization_RedirectsToErrorPageWhenNoCookieSupplied(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "example.com", nil)
|
||||
require.NoError(t, err)
|
||||
assertDirectAndHandlerNotCalled(t, req)
|
||||
}
|
||||
|
||||
func TestCheckAuthorization_RedirectsToErrorPageWhenCookieContainsInvalidJWT(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "example.com", nil)
|
||||
require.NoError(t, err)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: "not-a-jwt",
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
})
|
||||
assertDirectAndHandlerNotCalled(t, req)
|
||||
}
|
||||
|
||||
func TestCheckAuthorization_RedirectsToErrorPageWhenCookieContainsJWTWithIncorrectSignature(t *testing.T) {
|
||||
validUntil := time.Now().Add(time.Hour).UTC()
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
jwtClaimsValidUntilKey: validUntil.Format(time.RFC3339),
|
||||
})
|
||||
tokenStr, err := token.SignedString([]byte("wrong-key!"))
|
||||
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest("GET", "example.com", nil)
|
||||
require.NoError(t, err)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: tokenStr,
|
||||
Expires: validUntil,
|
||||
})
|
||||
assertDirectAndHandlerNotCalled(t, req)
|
||||
}
|
||||
|
||||
func TestCheckAuthorization_RedirectsToErrorPageWhenCookieContainsJWTWithMalformedClaims(t *testing.T) {
|
||||
validUntil := time.Now().Add(time.Hour).UTC()
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"wrong-key": validUntil.Format(time.RFC3339),
|
||||
})
|
||||
tokenStr, err := token.SignedString(appliance.jwtSecret)
|
||||
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest("GET", "example.com", nil)
|
||||
require.NoError(t, err)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: tokenStr,
|
||||
Expires: validUntil,
|
||||
})
|
||||
assertDirectAndHandlerNotCalled(t, req)
|
||||
}
|
||||
|
||||
func TestCheckAuthorization_RedirectsToErrorPageWhenCookieContainsJWTWithExpiredValidity(t *testing.T) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
jwtClaimsValidUntilKey: time.Now().Add(-1 * time.Hour).Format(time.RFC3339),
|
||||
})
|
||||
tokenStr, err := token.SignedString(appliance.jwtSecret)
|
||||
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest("GET", "example.com", nil)
|
||||
require.NoError(t, err)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: tokenStr,
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
})
|
||||
assertDirectAndHandlerNotCalled(t, req)
|
||||
}
|
||||
|
||||
func assertDirectAndHandlerNotCalled(t *testing.T, req *http.Request) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
require.Fail(t, "next handler should not be called")
|
||||
})
|
||||
respSpy := httptest.NewRecorder()
|
||||
appliance.CheckAuthorization(handler).ServeHTTP(respSpy, req)
|
||||
|
||||
require.Equal(t, http.StatusFound, respSpy.Code)
|
||||
func TestCheckAuthorization(t *testing.T) {
|
||||
// Create a mock Appliance
|
||||
mockAppliance := &Appliance{
|
||||
adminPasswordBcrypt: []byte("$2y$10$o2gHR6vUX7XPQj8tjUfi/e0zel.kpgvdTdSUkQthO9hTYooDUuoay"), // bcrypt hash for "password123"
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
expectedStatus int
|
||||
shouldCallNextHandler bool
|
||||
}{
|
||||
{
|
||||
name: "Valid password",
|
||||
password: "password123",
|
||||
expectedStatus: http.StatusOK,
|
||||
shouldCallNextHandler: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid password",
|
||||
password: "wrongpassword",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
shouldCallNextHandler: false,
|
||||
},
|
||||
{
|
||||
name: "Empty password",
|
||||
password: "",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
shouldCallNextHandler: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
nextHandlerCalled := false
|
||||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextHandlerCalled = true
|
||||
if !tt.shouldCallNextHandler {
|
||||
t.Error("Next handler should not be called after a 403")
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Set(authHeaderName, tt.password)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := mockAppliance.checkAuthorization(nextHandler)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != tt.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.expectedStatus)
|
||||
}
|
||||
|
||||
if tt.expectedStatus == http.StatusUnauthorized && nextHandlerCalled {
|
||||
t.Error("Next handler was called after a 403 response")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ go_library(
|
||||
"prometheus/default.yml.gotmpl",
|
||||
"postgres/codeinsights.conf",
|
||||
"grafana/default.yml.gotmpl",
|
||||
"otel/agent.yaml",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/config",
|
||||
tags = [TAG_INFRA_RELEASE],
|
||||
@ -25,6 +26,7 @@ go_library(
|
||||
"//lib/pointers",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||
"@io_k8s_sigs_controller_runtime//pkg/client",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@ -1,7 +1,37 @@
|
||||
package config
|
||||
|
||||
// Status is a point in the Appliance lifecycle that an Appliance can be in.
|
||||
type Status string
|
||||
|
||||
func (s Status) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
const (
|
||||
AnnotationKeyManaged = "appliance.sourcegraph.com/managed"
|
||||
AnnotationKeyCurrentVersion = "appliance.sourcegraph.com/currentVersion"
|
||||
AnnotationKeyConfigHash = "appliance.sourcegraph.com/configHash"
|
||||
ConfigmapName = "sourcegraph-appliance"
|
||||
|
||||
AnnotationKeyManaged = "appliance.sourcegraph.com/managed"
|
||||
AnnotationConditions = "appliance.sourcegraph.com/conditions"
|
||||
AnnotationKeyCurrentVersion = "appliance.sourcegraph.com/currentVersion"
|
||||
AnnotationKeyConfigHash = "appliance.sourcegraph.com/configHash"
|
||||
AnnotationKeyShouldTakeOwnership = "appliance.sourcegraph.com/adopted"
|
||||
|
||||
// TODO set status on configmap to communicate it across reboots
|
||||
AnnotationKeyStatus = "appliance.sourcegraph.com/status"
|
||||
|
||||
StatusUnknown Status = "unknown"
|
||||
StatusInstall Status = "install"
|
||||
StatusInstalling Status = "installing"
|
||||
StatusUpgrading Status = "upgrading"
|
||||
StatusWaitingForAdmin Status = "wait-for-admin"
|
||||
StatusRefresh Status = "refresh"
|
||||
StatusMaintenance Status = "maintenance"
|
||||
)
|
||||
|
||||
func IsPostInstallStatus(status Status) bool {
|
||||
switch status {
|
||||
case StatusUnknown, StatusInstall, StatusInstalling, StatusWaitingForAdmin:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package config
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type StandardComponent interface {
|
||||
Disableable
|
||||
@ -63,3 +66,20 @@ func (c StandardConfig) GetPrometheusPort() *int { return c.Prom
|
||||
func (c StandardConfig) GetServiceAccountAnnotations() map[string]string {
|
||||
return c.ServiceAccountAnnotations
|
||||
}
|
||||
|
||||
func MarkObjectForAdoption(obj client.Object) {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[AnnotationKeyShouldTakeOwnership] = "true"
|
||||
obj.SetAnnotations(annotations)
|
||||
}
|
||||
|
||||
func ShouldAdopt(obj client.Object) bool {
|
||||
if annotations := obj.GetAnnotations(); annotations != nil {
|
||||
_, ok := annotations[AnnotationKeyShouldTakeOwnership]
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed otel/*
|
||||
//go:embed postgres/*
|
||||
//go:embed prometheus/default.yml.gotmpl
|
||||
//go:embed grafana/default.yml.gotmpl
|
||||
@ -15,6 +16,7 @@ var (
|
||||
GrafanaDefaultConfigTemplate []byte
|
||||
CodeIntelConfig []byte
|
||||
CodeInsightsConfig []byte
|
||||
OtelAgentConfig []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -23,6 +25,7 @@ func init() {
|
||||
PgsqlConfig = mustReadFile("postgres/pgsql.conf")
|
||||
PrometheusDefaultConfigTemplate = mustReadFile("prometheus/default.yml.gotmpl")
|
||||
GrafanaDefaultConfigTemplate = mustReadFile("grafana/default.yml.gotmpl")
|
||||
OtelAgentConfig = mustReadFile("otel/agent.yaml")
|
||||
}
|
||||
|
||||
func mustReadFile(name string) []byte {
|
||||
|
||||
43
internal/appliance/config/otel/agent.yaml
Normal file
43
internal/appliance/config/otel/agent.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc: # port 4317
|
||||
http: # port 4318
|
||||
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: "otel-collector:4317"
|
||||
tls:
|
||||
insecure: true
|
||||
sending_queue:
|
||||
num_consumers: 4
|
||||
queue_size: 100
|
||||
retry_on_failure:
|
||||
enabled: true
|
||||
|
||||
# TODO: allow configuring processors through values
|
||||
#processors:
|
||||
# batch:
|
||||
# memory_limiter:
|
||||
# # 80% of maximum memory up to 2G
|
||||
# limit_mib: 400
|
||||
# # 25% of limit up to 2G
|
||||
# spike_limit_mib: 100
|
||||
# check_interval: 5s
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: ":13133"
|
||||
zpages:
|
||||
endpoint: "localhost:55679"
|
||||
|
||||
service:
|
||||
extensions:
|
||||
- zpages
|
||||
- health_check
|
||||
pipelines:
|
||||
traces:
|
||||
receivers:
|
||||
- otlp
|
||||
exporters:
|
||||
- otlp
|
||||
@ -88,6 +88,10 @@ type IndexedSearchSpec struct {
|
||||
Replicas int32 `json:"replicas,omitempty"`
|
||||
}
|
||||
|
||||
type OtelAgentSpec struct {
|
||||
StandardConfig
|
||||
}
|
||||
|
||||
type OtelCollectorSpec struct {
|
||||
StandardConfig
|
||||
}
|
||||
@ -228,7 +232,8 @@ type SourcegraphSpec struct {
|
||||
|
||||
Jaeger JaegerSpec `json:"jaeger,omitempty"`
|
||||
|
||||
OtelCollector OtelCollectorSpec `json:"openTelemetry,omitempty"`
|
||||
OtelAgent OtelAgentSpec `json:"openTelemetryAgent,omitempty"`
|
||||
OtelCollector OtelCollectorSpec `json:"openTelemetryCollector,omitempty"`
|
||||
|
||||
// PGSQL defines the desired state of the PostgreSQL database.
|
||||
PGSQL PGSQLSpec `json:"pgsql,omitempty"`
|
||||
@ -264,21 +269,33 @@ type SourcegraphSpec struct {
|
||||
StorageClass StorageClassSpec `json:"storageClass,omitempty"`
|
||||
}
|
||||
|
||||
// SetupStatus defines the observes status of the setup process.
|
||||
type SetupStatus struct {
|
||||
Progress int32
|
||||
// SourcegraphServicesToReconcile is a list of all Sourcegraph services that will be reconciled by appliance.
|
||||
var SourcegraphServicesToReconcile = []string{
|
||||
"blobstore",
|
||||
"repo-updater",
|
||||
"symbols",
|
||||
"gitserver",
|
||||
"redis",
|
||||
"pgsql",
|
||||
"syntect",
|
||||
"precise-code-intel",
|
||||
"code-insights-db",
|
||||
"code-intel-db",
|
||||
"prometheus",
|
||||
"cadvisor",
|
||||
"worker",
|
||||
"frontend",
|
||||
"searcher",
|
||||
"indexed-searcher",
|
||||
"grafana",
|
||||
"jaeger",
|
||||
"otel",
|
||||
}
|
||||
|
||||
// SourcegraphStatus defines the observed state of Sourcegraph
|
||||
type SourcegraphStatus struct {
|
||||
// CurrentVersion is the version of Sourcegraph currently running.
|
||||
CurrentVersion string `json:"currentVersion"`
|
||||
|
||||
// Setup tracks the progress of the setup process.
|
||||
Setup SetupStatus `json:"setup,omitempty"`
|
||||
|
||||
// Represents the latest available observations of Sourcegraph's current state.
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// Sourcegraph is the Schema for the Sourcegraph API
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
package appliance
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed web/static
|
||||
staticFiles embed.FS
|
||||
staticFS, _ = fs.Sub(staticFiles, "web/static")
|
||||
|
||||
//go:embed web/template
|
||||
templateFS embed.FS
|
||||
)
|
||||
@ -1,43 +1,41 @@
|
||||
package appliance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
)
|
||||
|
||||
const (
|
||||
queryKeyUserMessage = "sourcegraph-appliance-user-message"
|
||||
errMsgSomethingWentWrong = "Something went wrong - please contact support."
|
||||
)
|
||||
|
||||
func (a *Appliance) redirectToErrorPage(w http.ResponseWriter, req *http.Request, userMsg string, err error, userError bool) {
|
||||
a.redirectWithError(w, req, "/appliance/error", userMsg, err, userError)
|
||||
func (a *Appliance) logError(r *http.Request, err error) {
|
||||
a.logger.Error(err.Error(), log.String("method", r.Method), log.String("uri", r.URL.RequestURI()))
|
||||
}
|
||||
|
||||
func (a *Appliance) redirectWithError(w http.ResponseWriter, req *http.Request, path, userMsg string, err error, userError bool) {
|
||||
logFn := a.logger.Error
|
||||
if userError {
|
||||
logFn = a.logger.Info
|
||||
func (a *Appliance) errorResponse(w http.ResponseWriter, r *http.Request, status int, message any) {
|
||||
resp := responseData{"error": message}
|
||||
|
||||
if err := a.writeJSON(w, status, resp, nil); err != nil {
|
||||
a.logError(r, err)
|
||||
}
|
||||
logFn("an error occurred", log.Error(err))
|
||||
req = req.Clone(req.Context())
|
||||
req.URL.Path = path
|
||||
queryValues := req.URL.Query()
|
||||
queryValues.Set(queryKeyUserMessage, userMsg)
|
||||
req.URL.RawQuery = queryValues.Encode()
|
||||
http.Redirect(w, req, req.URL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *Appliance) errorHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if err := renderTemplate("error", w, struct {
|
||||
Msg string
|
||||
}{
|
||||
Msg: req.URL.Query().Get(queryKeyUserMessage),
|
||||
}); err != nil {
|
||||
a.handleError(w, err, "executing template")
|
||||
return
|
||||
}
|
||||
})
|
||||
func (a *Appliance) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
a.errorResponse(w, r, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
func (a *Appliance) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
a.logError(r, err)
|
||||
a.errorResponse(w, r, http.StatusInternalServerError, "the server encountered a problem and could not process your request")
|
||||
}
|
||||
|
||||
func (a *Appliance) notFoundResponse(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r, http.StatusNotFound, "the requested resource could not be found")
|
||||
}
|
||||
|
||||
func (a *Appliance) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r, http.StatusMethodNotAllowed, fmt.Sprintf("the %s method is not supported", r.Method))
|
||||
}
|
||||
|
||||
func (a *Appliance) invalidAdminPasswordResponse(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r, http.StatusUnauthorized, "invalid admin password")
|
||||
}
|
||||
|
||||
@ -3,6 +3,9 @@ load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
load("@npm//internal/appliance/frontend/maintenance:tsconfig-to-swcconfig/package_json.bzl", tsconfig_to_swcconfig = "bin")
|
||||
load("@npm//internal/appliance/frontend/maintenance:vite/package_json.bzl", vite_bin = "bin")
|
||||
load("@rules_pkg//:pkg.bzl", "pkg_tar")
|
||||
load("//wolfi-images:defs.bzl", "wolfi_base")
|
||||
load("@container_structure_test//:defs.bzl", "container_structure_test")
|
||||
|
||||
npm_link_all_packages(
|
||||
name = "node_modules",
|
||||
@ -19,13 +22,12 @@ RUNTIME_DEPS = [
|
||||
"src/Install.tsx",
|
||||
"src/Login.tsx",
|
||||
"src/Maintenance.tsx",
|
||||
"src/OperatorDebugBar.tsx",
|
||||
"src/OperatorStatus.tsx",
|
||||
"src/Progress.tsx",
|
||||
"src/Theme.tsx",
|
||||
"src/WaitForAdmin.tsx",
|
||||
"src/api.ts",
|
||||
"src/debugBar.ts",
|
||||
"src/state.ts",
|
||||
"src/index.css",
|
||||
"src/main.tsx",
|
||||
"src/reportWebVitals.ts",
|
||||
@ -104,6 +106,7 @@ js_run_binary(
|
||||
mnemonic = "ViteBuild",
|
||||
out_dirs = ["dist"],
|
||||
tool = ":vite",
|
||||
visibility = ["//docker-images/appliance-frontend:__pkg__"],
|
||||
)
|
||||
|
||||
# Hosts the production-bundled application in a web server
|
||||
@ -113,3 +116,38 @@ vite_bin.vite_binary(
|
||||
chdir = package_name(),
|
||||
data = [":build"],
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "tar_frontend",
|
||||
srcs = [":build"],
|
||||
package_dir = "maintenance",
|
||||
strip_prefix = "dist",
|
||||
visibility = ["//docker-images/appliance-frontend:__pkg__"],
|
||||
)
|
||||
|
||||
container_structure_test(
|
||||
name = "image_test",
|
||||
timeout = "short",
|
||||
configs = ["image_test.yaml"],
|
||||
driver = "docker",
|
||||
image = "//docker-images/appliance-frontend:image",
|
||||
tags = [
|
||||
"exclusive",
|
||||
"requires-network",
|
||||
TAG_INFRA_DEVINFRA,
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "config",
|
||||
srcs = ["maintenance.conf.template"],
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "tar_config",
|
||||
srcs = [":config"],
|
||||
package_dir = "/etc/nginx/templates",
|
||||
visibility = ["//docker-images/appliance-frontend:__pkg__"],
|
||||
)
|
||||
|
||||
wolfi_base(target = "appliance-frontend")
|
||||
|
||||
@ -12,3 +12,17 @@ This will run the service locally, starting a Vite developer environment:
|
||||
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
|
||||
## Wolfi image
|
||||
|
||||
This will build and test the Wolfi image:
|
||||
|
||||
### Building
|
||||
|
||||
bazel build //docker-images/appliance-frontend:image
|
||||
|
||||
### Testing
|
||||
|
||||
bazel test \
|
||||
//internal/appliance/frontend/maintenance:image_test \
|
||||
//docker-images/appliance-frontend:image_test
|
||||
|
||||
12
internal/appliance/frontend/maintenance/image_test.yaml
Executable file
12
internal/appliance/frontend/maintenance/image_test.yaml
Executable file
@ -0,0 +1,12 @@
|
||||
schemaVersion: "2.0.0"
|
||||
|
||||
commandTests:
|
||||
- name: maintenance server available
|
||||
command: /init.sh
|
||||
args:
|
||||
- stat
|
||||
- /etc/nginx/conf.d/maintenance.conf
|
||||
- name: maintenance app is available
|
||||
command: stat
|
||||
args:
|
||||
- /maintenance/index.html
|
||||
49
internal/appliance/frontend/maintenance/maintenance.conf.template
Executable file
49
internal/appliance/frontend/maintenance/maintenance.conf.template
Executable file
@ -0,0 +1,49 @@
|
||||
# ____ ___ ___ _ _ ____ _ _ ____ ____
|
||||
# |__| |__] |__] | | |__| |\ | | |___
|
||||
# | | | | |___ | | | | \| |___ |___
|
||||
#
|
||||
# _ _ ____ _ _ _ ___ ____ _ _ ____ _ _ ____ ____
|
||||
# |\/| |__| | |\ | | |___ |\ | |__| |\ | | |___
|
||||
# | | | | | | \| | |___ | \| | | | \| |___ |___
|
||||
#
|
||||
# Sourcegraph Appliance Maintenance UI
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
access_log off;
|
||||
|
||||
|
||||
location / {
|
||||
# Hideous char-mask to avoid nested ifs, which casue warnings in various
|
||||
# config linters. nginx doesn't support boolean operators as far as I
|
||||
# can tell.
|
||||
set $redirect_mask 0;
|
||||
if ($request_uri !~ ^/maintenance) {
|
||||
set $redirect_mask 1;
|
||||
}
|
||||
if ($request_uri !~ ^/api) {
|
||||
set $redirect_mask 1$redirect_mask;
|
||||
}
|
||||
if ($request_uri !~ ^/assets) {
|
||||
set $redirect_mask 1$redirect_mask;
|
||||
}
|
||||
if ($redirect_mask = 111) {
|
||||
return 302 $scheme://$host:$server_port/maintenance;
|
||||
}
|
||||
|
||||
root /maintenance;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass ${API_ENDPOINT}/api/;
|
||||
}
|
||||
|
||||
error_page 404 /;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /maintenance;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { AppBar, Typography, useTheme } from '@mui/material'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
@ -7,12 +7,11 @@ import logo from '../assets/sourcegraph.png'
|
||||
|
||||
import { adminPassword, call } from './api'
|
||||
import { Login } from './Login'
|
||||
import { OperatorDebugBar } from './OperatorDebugBar'
|
||||
import { OperatorStatus } from './OperatorStatus'
|
||||
import { Info } from './Theme'
|
||||
|
||||
const FetchStateTimerMs = 1 * 1000
|
||||
const WaitToLoginAfterConnectMs = 1 * 1000
|
||||
const FetchStateTimerMs = 1000
|
||||
const WaitToLoginAfterConnectMs = 1000
|
||||
|
||||
export type stage = 'unknown' | 'install' | 'installing' | 'wait-for-admin' | 'upgrading' | 'maintenance' | 'refresh'
|
||||
|
||||
@ -29,7 +28,7 @@ export interface OutletContext {
|
||||
|
||||
const fetchStatus = async (lastContext: OutletContext): Promise<OutletContext> =>
|
||||
new Promise<OutletContext>(resolve => {
|
||||
call('/api/operator/v1beta1/stage')
|
||||
call('/api/v1/appliance/status')
|
||||
.then(result => {
|
||||
if (!result.ok) {
|
||||
if (result.status === 401) {
|
||||
@ -39,7 +38,7 @@ const fetchStatus = async (lastContext: OutletContext): Promise<OutletContext> =
|
||||
onlineDate: lastContext.onlineDate ?? Date.now(),
|
||||
})
|
||||
} else {
|
||||
resolve({ online: false, onlineDate: undefined })
|
||||
resolve({ online: false, onlineDate: undefined, stage: 'refresh' })
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -49,12 +48,12 @@ const fetchStatus = async (lastContext: OutletContext): Promise<OutletContext> =
|
||||
.then(result => {
|
||||
resolve({
|
||||
online: true,
|
||||
stage: result.stage,
|
||||
stage: result.status.status,
|
||||
onlineDate: lastContext.onlineDate ?? Date.now(),
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
resolve({ online: false, onlineDate: undefined })
|
||||
resolve({ online: false, onlineDate: undefined, stage: 'refresh' })
|
||||
})
|
||||
})
|
||||
|
||||
@ -102,9 +101,9 @@ export const Frame: React.FC = () => {
|
||||
<div id="frame">
|
||||
<AppBar color="secondary">
|
||||
<div className="product">
|
||||
<img id="logo" src={logo} />
|
||||
<img id="logo" src={logo} alt={'Sourcegraph logo'} />
|
||||
<Typography className={`title-${theme.palette.mode}`} variant="h6">
|
||||
Appliance
|
||||
Sourcegraph Appliance
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="spacer" />
|
||||
@ -119,7 +118,6 @@ export const Frame: React.FC = () => {
|
||||
<Outlet context={context} />
|
||||
)}
|
||||
</div>
|
||||
<OperatorDebugBar context={context} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import { CircularProgress, Typography } from '@mui/material'
|
||||
|
||||
import './App.css'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
|
||||
import { OutletContext } from './Frame'
|
||||
|
||||
@ -1,86 +1,186 @@
|
||||
import { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import { Button, Checkbox, FormControl, InputLabel, MenuItem, Paper, Select, Stack, Typography } from '@mui/material'
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
RadioGroup,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Typography,
|
||||
Radio,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
FormHelperText,
|
||||
Box,
|
||||
TextField,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@mui/material'
|
||||
|
||||
import search from '../assets/sourcegraph.png'
|
||||
|
||||
import { changeStage } from './debugBar'
|
||||
|
||||
interface InstallerProps {
|
||||
allowDisable: boolean
|
||||
}
|
||||
import { changeStage } from './state'
|
||||
|
||||
export const Install: React.FC = () => {
|
||||
const [version, setVersion] = useState<string>('5.3.1')
|
||||
const [installSearch, setInstallSearch] = useState<boolean>(true)
|
||||
type installState = 'select-version' | 'select-db-type'
|
||||
const [installState, setInstallState] = useState<installState>('select-version')
|
||||
|
||||
const install = () => {
|
||||
changeStage({ action: 'installing', data: version })
|
||||
const [versions, setVersions] = useState<string[]>([])
|
||||
const [selectedVersion, setSelectedVersion] = useState<string>('')
|
||||
|
||||
type dbType = 'built-in' | 'external'
|
||||
const [dbType, setDbType] = useState<dbType>('built-in')
|
||||
|
||||
type dbTab = 'pgsql' | 'codeintel' | 'codeinsights'
|
||||
const [dbTab, setDbTab] = useState<dbTab>('pgsql')
|
||||
|
||||
const handleDbTabChange = (event: React.SyntheticEvent, newValue: dbTab) => {
|
||||
setDbTab(newValue)
|
||||
}
|
||||
|
||||
const SearchInstaller: React.FC<InstallerProps> = ({ allowDisable = false }) => (
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
gap: 2,
|
||||
}}
|
||||
onClick={allowDisable ? () => setInstallSearch(prevSarch => !prevSarch) : undefined}
|
||||
>
|
||||
<img src={search} />
|
||||
<Stack sx={{ flex: 1 }}>
|
||||
<Typography variant="subtitle2">
|
||||
<b>Search Suite</b>
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
Sourcegraph search suite: Code Search, Code Intelligence, <br />
|
||||
Batch Changes, and Own.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Checkbox sx={{ p: 0 }} color="default" size="small" checked={installSearch} />
|
||||
</Paper>
|
||||
)
|
||||
useEffect(() => {
|
||||
const fetchVersions = async () => {
|
||||
try {
|
||||
const response = await fetch('https://releaseregistry.sourcegraph.com/v1/releases/sourcegraph', {
|
||||
headers: {
|
||||
Authorization: `Bearer token`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
mode: 'cors',
|
||||
})
|
||||
const data = await response.json()
|
||||
setVersions(data)
|
||||
if (data.length > 0) {
|
||||
const publicVersions = data
|
||||
.filter(item => item.public)
|
||||
.filter(item => !item.is_development)
|
||||
.map(item => item.version)
|
||||
setVersions(publicVersions)
|
||||
setSelectedVersion(publicVersions[0]) // Set the first version as default
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch versions:', error)
|
||||
|
||||
const allowInstall = installSearch
|
||||
// Very basic fallback for when release registry is down:
|
||||
// hardcode a particular version of Sourcegraph, which is the
|
||||
// latest at the time of writing.
|
||||
// This could be replaced with a fallback to a release registry
|
||||
// response fixture that appliance-frontend has access to on the
|
||||
// filesystem. In Kubernetes, this could be derived from a
|
||||
// ConfigMap, with the files being distributed to airgap users
|
||||
// out-of-band.
|
||||
const publicVersions = ['v5.5.2463']
|
||||
setVersions(publicVersions)
|
||||
setSelectedVersion(publicVersions[0])
|
||||
}
|
||||
}
|
||||
|
||||
fetchVersions()
|
||||
}, [])
|
||||
|
||||
const next = () => {
|
||||
if (selectedVersion === '') {
|
||||
alert('Please select a version')
|
||||
return
|
||||
}
|
||||
setInstallState('select-db-type')
|
||||
}
|
||||
|
||||
const back = () => {
|
||||
setInstallState('select-version')
|
||||
}
|
||||
|
||||
const install = () => {
|
||||
changeStage({ action: 'installing', data: selectedVersion })
|
||||
}
|
||||
|
||||
const handleDbSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDbType(event.target.value as dbType)
|
||||
}
|
||||
|
||||
return (
|
||||
// Render a version selection box followed by a database configuration screen, then an install prompt
|
||||
<div className="install">
|
||||
<Typography variant="h5">Install Sourcegraph Appliance</Typography>
|
||||
<Typography variant="h5">Setup Sourcegraph</Typography>
|
||||
<Paper elevation={3} sx={{ p: 4 }}>
|
||||
<Stack direction="column" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<FormControl sx={{ minWidth: 200 }}>
|
||||
<InputLabel id="demo-simple-select-label">Version</InputLabel>
|
||||
<Select
|
||||
value={version}
|
||||
label="Age"
|
||||
onChange={e => setVersion(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
>
|
||||
<MenuItem value={'5.3.1'}>5.3.1</MenuItem>
|
||||
<MenuItem value={'5.4.0'}>5.4.0 [Merge Demo Only]</MenuItem>
|
||||
<MenuItem value={'5.4.1 (beta)'}>5.4.0 (beta) [Merge Demo Only]</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography variant="subtitle1">Select Components To Install</Typography>
|
||||
<div className="components">
|
||||
<SearchInstaller allowDisable={false} />
|
||||
</div>
|
||||
<div className="message">
|
||||
{allowInstall ? (
|
||||
<Typography variant="caption">Press install to begin installation.</Typography>
|
||||
) : (
|
||||
<Typography variant="caption" color="error">
|
||||
Please select at least one component to install.
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="contained" sx={{ width: 200 }} onClick={install} disabled={!allowInstall}>
|
||||
Install
|
||||
</Button>
|
||||
</Stack>
|
||||
{installState === 'select-version' ? (
|
||||
<Stack direction="column" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<FormControl sx={{ minWidth: 200 }}>
|
||||
<InputLabel id="demo-simple-select-label">Version</InputLabel>
|
||||
<Select
|
||||
value={selectedVersion}
|
||||
label="Version"
|
||||
onChange={e => setSelectedVersion(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
>
|
||||
{versions.map(version => (
|
||||
<MenuItem key={version} value={version}>
|
||||
{version}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<div className="message">
|
||||
<Typography variant="caption">Proceed to database configuration.</Typography>
|
||||
</div>
|
||||
<Button variant="contained" sx={{ width: 200 }} onClick={next}>
|
||||
Next
|
||||
</Button>
|
||||
</Stack>
|
||||
) : installState === 'select-db-type' ? (
|
||||
<Stack direction="column" spacing={2} alignItems={'center'}>
|
||||
<FormControl>
|
||||
<FormLabel>Configure Sourcegraph Databases</FormLabel>
|
||||
<FormGroup>
|
||||
<RadioGroup value={dbType} onChange={handleDbSelect} defaultValue="built-in">
|
||||
<FormControlLabel value="built-in" control={<Radio />} label="built-in DBs" />
|
||||
<FormHelperText id="my-helper-text" fontSize="small">
|
||||
Selecting built-in dbs, configures sourcegraph to use built in databases.
|
||||
Provisioned and controlled directly by appliance.{' '}
|
||||
</FormHelperText>
|
||||
<FormControlLabel
|
||||
value="external"
|
||||
control={<Radio />}
|
||||
label="External DBs (not yet supported)"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
{dbType === 'external' ? (
|
||||
<Box sx={{ width: '80%' }} alignContent={'center'}>
|
||||
<Box
|
||||
alignContent={'center'}
|
||||
sx={{ paddingBottom: 2.5, borderBottom: 1, borderColor: 'divider' }}
|
||||
>
|
||||
<Tabs value={dbTab} onChange={handleDbTabChange}>
|
||||
<Tab label="Pgsql" disabled />
|
||||
<Tab label="Codeintel-db" disabled />
|
||||
<Tab label="Codeinsights-db" disabled />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<FormGroup>
|
||||
<Stack spacing={2}>
|
||||
<TextField disabled label="Port" defaultValue="5432" />
|
||||
<TextField disabled label="User" defaultValue="sg" />
|
||||
<TextField disabled label="Password" defaultValue="sg" />
|
||||
<TextField disabled label="Database" defaultValue="sg" />
|
||||
<TextField disabled label="SSL Mode" defaultValue="disable" />
|
||||
</Stack>
|
||||
</FormGroup>
|
||||
</Box>
|
||||
) : null}
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button variant="contained" sx={{ width: 200 }} onClick={back}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="contained" sx={{ width: 200 }} onClick={install}>
|
||||
Install
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : null}
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createRef, useEffect, useState } from 'react'
|
||||
import React, { createRef, useEffect, useState } from 'react'
|
||||
|
||||
import Maintenance from '@mui/icons-material/Engineering'
|
||||
import { Box, Button, Paper, TextField, Typography } from '@mui/material'
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type React from 'react'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
|
||||
import Unhealthy from '@mui/icons-material/CarCrashOutlined'
|
||||
@ -6,10 +7,10 @@ import { Alert, Button, CircularProgress, Grid, Stack, Typography } from '@mui/m
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { call } from './api'
|
||||
import { maintenance } from './debugBar'
|
||||
import { maintenance } from './state'
|
||||
|
||||
const MaintenanceStatusTimerMs = 1 * 1000
|
||||
const WaitToLaunchFixMs = 5 * 1000
|
||||
const MaintenanceStatusTimerMs = 1000
|
||||
const WaitToLaunchFixMs = 5000
|
||||
|
||||
type Service = {
|
||||
name: string
|
||||
@ -17,7 +18,7 @@ type Service = {
|
||||
message: string
|
||||
}
|
||||
|
||||
type Status = {
|
||||
type ServiceStatuses = {
|
||||
services: Service[]
|
||||
}
|
||||
|
||||
@ -54,14 +55,14 @@ const ShowServices: React.FC<{ services: Service[] }> = ({ services }) =>
|
||||
) : null
|
||||
|
||||
export const Maintenance: React.FC = () => {
|
||||
const [status, setStatus] = useState<Status | undefined>()
|
||||
const [serviceStatuses, setServiceStatuses] = useState<ServiceStatuses | undefined>()
|
||||
const [fixing, setFixing] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
call('/api/operator/v1beta1/maintenance/status')
|
||||
call('/api/v1/appliance/maintenance/serviceStatuses')
|
||||
.then(response => response.json())
|
||||
.then(setStatus)
|
||||
.then(serviceStatuses => setServiceStatuses(serviceStatuses))
|
||||
}, MaintenanceStatusTimerMs)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
@ -75,8 +76,8 @@ export const Maintenance: React.FC = () => {
|
||||
}
|
||||
}, [fixing])
|
||||
|
||||
const ready = status?.services.length !== undefined
|
||||
const unhealthy = status?.services?.find((s: Service) => !s.healthy)
|
||||
const ready = serviceStatuses?.services.length !== undefined
|
||||
const unhealthy = serviceStatuses?.services?.find((s: Service) => !s.healthy)
|
||||
|
||||
return (
|
||||
<div className="maintenance">
|
||||
@ -97,7 +98,7 @@ export const Maintenance: React.FC = () => {
|
||||
{ready ? (
|
||||
<>
|
||||
<Typography variant="h5">Service Status</Typography>
|
||||
<ShowServices services={status?.services ?? []} />
|
||||
<ShowServices services={serviceStatuses?.services ?? []} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Button, Paper, Stack, Typography } from '@mui/material'
|
||||
|
||||
import { call } from './api'
|
||||
import { changeStage, maintenance } from './debugBar'
|
||||
import { ContextProps, stage } from './Frame'
|
||||
|
||||
const DebugBarTimerMs = 1 * 1000
|
||||
|
||||
export const OperatorDebugBar: React.FC<ContextProps> = ({ context }) => {
|
||||
const [waiting, setWaiting] = useState(false)
|
||||
|
||||
const setStage = (action: stage, data?: string) => changeStage({ action, data, onDone: () => setWaiting(true) })
|
||||
|
||||
const startInstall = () => setStage('install')
|
||||
const installProgress = () => setStage('installing')
|
||||
const installWaitAdmin = () => setStage('wait-for-admin')
|
||||
const upgradeProgress = () => setStage('upgrading', '5.4.0 (beta1)')
|
||||
const noState = () => setStage('unknown')
|
||||
const launchAdminUI = () => setStage('refresh')
|
||||
const failInstall = () => {
|
||||
call('/api/operator/v1beta1/fake/install/fail', {
|
||||
method: 'POST',
|
||||
}).then(() => {
|
||||
setWaiting(true)
|
||||
})
|
||||
}
|
||||
const setMaintenance = ({ healthy }: { healthy: boolean }) =>
|
||||
maintenance({ healthy, onDone: () => setWaiting(true) })
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
if (waiting) {
|
||||
setWaiting(false)
|
||||
}
|
||||
}, DebugBarTimerMs)
|
||||
return () => clearInterval(timer)
|
||||
}, [waiting])
|
||||
|
||||
const showDebugBar = localStorage.getItem('debugbar') === 'true'
|
||||
|
||||
return (
|
||||
context.online &&
|
||||
showDebugBar && (
|
||||
<Paper id="operator-debug" elevation={3} sx={{ m: 1, p: 2 }}>
|
||||
<Stack direction="column" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<Typography variant="caption">Operator Debug Controls</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Stack sx={{ alignItems: 'center', p: 1, border: '1px solid lightgray' }}>
|
||||
<Typography variant="caption">Installation</Typography>
|
||||
<Stack direction="row">
|
||||
<Stack direction="column">
|
||||
<Button disabled={waiting} onClick={startInstall}>
|
||||
Start
|
||||
</Button>
|
||||
<Button disabled={waiting} onClick={installProgress}>
|
||||
Progress...
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack direction="column">
|
||||
<Button disabled={waiting} onClick={installWaitAdmin}>
|
||||
Wait for admin
|
||||
</Button>
|
||||
<Button disabled={waiting} onClick={failInstall}>
|
||||
Crash
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack sx={{ alignItems: 'center', p: 1, border: '1px solid lightgray' }}>
|
||||
<Typography variant="caption">Maintenance</Typography>
|
||||
<Button disabled={waiting} onClick={() => setMaintenance({ healthy: false })}>
|
||||
Unhealthy
|
||||
</Button>
|
||||
<Button disabled={waiting} onClick={() => setMaintenance({ healthy: true })}>
|
||||
Healthy
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
p: 1,
|
||||
border: '1px solid lightgray',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">Reset</Typography>
|
||||
<Button disabled={waiting} onClick={noState}>
|
||||
Reset
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
p: 1,
|
||||
border: '1px solid lightgray',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">Upgrade</Typography>
|
||||
<Button disabled={waiting} onClick={upgradeProgress}>
|
||||
Start
|
||||
</Button>
|
||||
<Button disabled={waiting} onClick={launchAdminUI}>
|
||||
Finish
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
import styledReact from '@emotion/styled'
|
||||
import { styled } from '@mui/material'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
@ -20,7 +22,7 @@ export const OperatorStatus: React.FC<ContextProps> = ({ context }) => {
|
||||
const Status = () =>
|
||||
context.online === undefined ? (
|
||||
<div className="status connecting">connecting</div>
|
||||
) : context.online === true || context.needsLogin === true ? (
|
||||
) : context.online || context.needsLogin ? (
|
||||
<div className="status online">
|
||||
<OnlineIcon />
|
||||
</div>
|
||||
@ -32,14 +34,14 @@ export const OperatorStatus: React.FC<ContextProps> = ({ context }) => {
|
||||
|
||||
switch (context.stage) {
|
||||
case 'refresh':
|
||||
document.location = '/?cacheBust=' + Date.now()
|
||||
document.location.reload()
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="operator-status">
|
||||
Status: <Status />
|
||||
{context.online === false && <Navigate to="/" />}
|
||||
{!context.online && <Navigate to="/" />}
|
||||
{context.stage === 'unknown' && <Navigate to="/" />}
|
||||
{context.stage === 'install' && <Navigate to="/install" />}
|
||||
{context.stage === 'installing' && <Navigate to="/install/progress" />}
|
||||
|
||||
@ -92,13 +92,13 @@ export const Progress: React.FC<{
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
call('/api/operator/v1beta1/install/progress')
|
||||
call('/api/v1/appliance/install/progress')
|
||||
.then(result => result.json())
|
||||
.then(result => {
|
||||
setVersion(result.version)
|
||||
setProgress(result.progress)
|
||||
setError(result.error)
|
||||
setTasks(result.tasks)
|
||||
setVersion(result.progress.version)
|
||||
setProgress(result.progress.progress)
|
||||
setError(result.progress.error)
|
||||
setTasks(result.progress.tasks)
|
||||
})
|
||||
.catch(err => setError(err.message))
|
||||
}, 1000)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren, createContext, useContext, useMemo, useState } from 'react'
|
||||
import React, { PropsWithChildren, createContext, useContext, useMemo, useState } from 'react'
|
||||
|
||||
import { DarkModeOutlined, LightModeOutlined } from '@mui/icons-material'
|
||||
import { CssBaseline, ThemeProvider as MuiThemeProvider, PaletteMode, Theme, createTheme } from '@mui/material'
|
||||
@ -15,7 +15,7 @@ export const Context = createContext<ThemeContextProps>({
|
||||
theme: createTheme(),
|
||||
})
|
||||
|
||||
export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
export const ThemeProvider: React.FC<PropsWithChildren<any>> = ({ children }) => {
|
||||
const [mode, setMode] = useState<PaletteMode>((localStorage.getItem('theme') as PaletteMode) ?? 'light')
|
||||
|
||||
const theme = useMemo(() => {
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { Button, CircularProgress, Stack, Typography } from '@mui/material'
|
||||
|
||||
import { changeStage } from './debugBar'
|
||||
import { changeStage } from './state.ts'
|
||||
|
||||
const TestAdminUIGoodMs = 1 * 1000
|
||||
const WaitBeforeLaunchMs = 3 * 1000
|
||||
|
||||
export const WaitForAdmin: React.FC = () => {
|
||||
const [waitingForBalancer, setWaitingForBalancer] = useState<boolean>(false)
|
||||
const [launching, setLaunching] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
@ -20,24 +18,9 @@ export const WaitForAdmin: React.FC = () => {
|
||||
}
|
||||
}, [launching])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
fetch('/sign-in')
|
||||
.then(result => {
|
||||
console.log('waiting for admin ui', result)
|
||||
if (result.ok) {
|
||||
setLaunching(true)
|
||||
setWaitingForBalancer(false)
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
}, TestAdminUIGoodMs)
|
||||
return () => clearInterval(timer)
|
||||
}, [waitingForBalancer])
|
||||
|
||||
return (
|
||||
<div className="wait-for-admin">
|
||||
<Typography variant="h5">Waiting For The Admin To Return</Typography>
|
||||
<Typography variant="h4">Waiting For The Admin To Return</Typography>
|
||||
<div>
|
||||
<Typography sx={{ m: 2 }}>
|
||||
The appliance is ready. We were waiting for you to set its security before opening it up.
|
||||
@ -46,11 +29,7 @@ export const WaitForAdmin: React.FC = () => {
|
||||
Now that you're back, please press the button below to launch the Administration UI.
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setWaitingForBalancer(true)}
|
||||
disabled={launching || waitingForBalancer}
|
||||
>
|
||||
<Button variant="contained" onClick={() => setLaunching(true)} disabled={launching}>
|
||||
Launch Admin UI
|
||||
</Button>
|
||||
{launching && (
|
||||
@ -59,12 +38,6 @@ export const WaitForAdmin: React.FC = () => {
|
||||
<Typography variant="h5">Launching Admin UI... Please wait...</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
{waitingForBalancer && (
|
||||
<Stack direction="row" spacing={2}>
|
||||
<CircularProgress size={32} />
|
||||
<Typography variant="h5">Waiting for Admin UI to be ready... Please wait...</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import { call } from './api'
|
||||
import { stage } from './Frame'
|
||||
|
||||
export const maintenance = ({ healthy, onDone }: { healthy: boolean; onDone?: () => void }): Promise<void> => {
|
||||
return call('/api/operator/v1beta1/fake/maintenance/healthy', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ healthy: healthy }),
|
||||
})
|
||||
.then(() => {
|
||||
call('/api/operator/v1beta1/fake/stage', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ stage: 'maintenance' }),
|
||||
}).then(() => {
|
||||
if (onDone !== undefined) {
|
||||
onDone()
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
if (onDone !== undefined) {
|
||||
onDone()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const changeStage = ({ action, data, onDone }: { action: stage; data?: string; onDone?: () => void }) => {
|
||||
call('/api/operator/v1beta1/fake/stage', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ stage: action, data }),
|
||||
}).then(() => {
|
||||
if (onDone) {
|
||||
onDone()
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import { ReportHandler } from 'web-vitals'
|
||||
|
||||
const reportWebVitals = (onPerfEntry: ReportHandler) => {
|
||||
if (onPerfEntry) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
|
||||
31
internal/appliance/frontend/maintenance/src/state.ts
Normal file
31
internal/appliance/frontend/maintenance/src/state.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { call } from './api'
|
||||
import { stage } from './Frame'
|
||||
|
||||
export const maintenance = async ({ healthy, onDone }: { healthy: boolean; onDone?: () => void }): Promise<void> => {
|
||||
await call('/api/operator/v1beta1/fake/maintenance/healthy', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ healthy: healthy }),
|
||||
})
|
||||
call('/v1/appliance/status', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ stage: 'maintenance' }),
|
||||
}).then(() => {
|
||||
if (onDone !== undefined) {
|
||||
onDone()
|
||||
}
|
||||
})
|
||||
if (onDone !== undefined) {
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
export const changeStage = ({ action, data, onDone }: { action: stage; data?: string; onDone?: () => void }) => {
|
||||
call('/api/v1/appliance/status', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ state: action, data }),
|
||||
}).then(() => {
|
||||
if (onDone) {
|
||||
onDone()
|
||||
}
|
||||
})
|
||||
}
|
||||
44
internal/appliance/healthchecker/BUILD.bazel
Normal file
44
internal/appliance/healthchecker/BUILD.bazel
Normal file
@ -0,0 +1,44 @@
|
||||
load("//dev:go_defs.bzl", "go_test")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "healthchecker",
|
||||
srcs = [
|
||||
"health_checker.go",
|
||||
"probe.go",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/healthchecker",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//lib/errors",
|
||||
"@com_github_sourcegraph_log//:log",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apimachinery//pkg/labels",
|
||||
"@io_k8s_sigs_controller_runtime//pkg/client",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "healthchecker_test",
|
||||
srcs = ["health_checker_test.go"],
|
||||
data = [
|
||||
"//dev/tools:kubebuilder-assets",
|
||||
],
|
||||
embed = [":healthchecker"],
|
||||
env = {
|
||||
"KUBEBUILDER_ASSET_PATHS": "$(rlocationpaths //dev/tools:kubebuilder-assets)",
|
||||
},
|
||||
deps = [
|
||||
"//internal/appliance/k8senvtest",
|
||||
"//internal/k8s/resource/service",
|
||||
"@com_github_sourcegraph_log//:log",
|
||||
"@com_github_sourcegraph_log//logtest",
|
||||
"@com_github_sourcegraph_log_logr//:logr",
|
||||
"@com_github_stretchr_testify//require",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||
"@io_k8s_apimachinery//pkg/types",
|
||||
"@io_k8s_apimachinery//pkg/util/intstr",
|
||||
"@io_k8s_sigs_controller_runtime//pkg/client",
|
||||
],
|
||||
)
|
||||
95
internal/appliance/healthchecker/health_checker.go
Normal file
95
internal/appliance/healthchecker/health_checker.go
Normal file
@ -0,0 +1,95 @@
|
||||
package healthchecker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
type Probe interface {
|
||||
CheckPods(ctx context.Context, labelSelector, namespace string) error
|
||||
}
|
||||
|
||||
type HealthChecker struct {
|
||||
Probe Probe
|
||||
K8sClient client.Client
|
||||
Logger log.Logger
|
||||
|
||||
ServiceName client.ObjectKey
|
||||
Interval time.Duration
|
||||
Graceperiod time.Duration
|
||||
}
|
||||
|
||||
// ManageIngressFacingService waits for the `begin` channel to close, then periodically monitors the frontend
|
||||
// service (the ingress-facing service). When there is at least one ready
|
||||
// frontend pod, it ensures that the service points at the frontend pods. When
|
||||
// there are no ready pods, it ensures that the service points to the appliance,
|
||||
// so that the admin can log in and view maintenance status.
|
||||
func (h *HealthChecker) ManageIngressFacingService(ctx context.Context, begin <-chan struct{}, labelSelector, namespace string) error {
|
||||
h.Logger.Info("waiting for signal to begin managing ingress-facing service for the appliance")
|
||||
select {
|
||||
case <-begin:
|
||||
// block
|
||||
|
||||
case <-ctx.Done():
|
||||
h.Logger.Error("context done, exiting", log.Error(ctx.Err()))
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
h.Logger.Info("will periodically check health of frontend and re-point ingress appropriately")
|
||||
|
||||
ticker := time.NewTicker(h.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Do one iteration without having to wait for the first tick
|
||||
if err := h.maybeFlipServiceOnce(ctx, labelSelector, namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := h.maybeFlipServiceOnce(ctx, labelSelector, namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
h.Logger.Error("context done, exiting", log.Error(ctx.Err()))
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HealthChecker) maybeFlipServiceOnce(ctx context.Context, labelSelector, namespace string) error {
|
||||
h.Logger.Info("checking deployment health")
|
||||
if err := h.Probe.CheckPods(ctx, labelSelector, namespace); err != nil {
|
||||
h.Logger.Error("found unhealthy state, waiting for the grace period", log.Error(err), log.String("gracePeriod", h.Graceperiod.String()))
|
||||
time.Sleep(h.Graceperiod)
|
||||
if err := h.Probe.CheckPods(ctx, labelSelector, namespace); err != nil {
|
||||
h.Logger.Error("found unhealthy state, setting service selector to appliance", log.Error(err))
|
||||
return h.setServiceSelector(ctx, "sourcegraph-appliance-frontend")
|
||||
}
|
||||
}
|
||||
|
||||
h.Logger.Info("deployment healthy")
|
||||
return h.setServiceSelector(ctx, "sourcegraph-frontend")
|
||||
}
|
||||
|
||||
func (h *HealthChecker) setServiceSelector(ctx context.Context, to string) error {
|
||||
h.Logger.Info("setting service selector", log.String("to", to))
|
||||
|
||||
var svc corev1.Service
|
||||
if err := h.K8sClient.Get(ctx, h.ServiceName, &svc); err != nil {
|
||||
h.Logger.Error("getting service", log.Error(err))
|
||||
return errors.Wrap(err, "getting service")
|
||||
}
|
||||
|
||||
// no-op if the selector is unchanged
|
||||
svc.Spec.Selector["app"] = to
|
||||
return h.K8sClient.Update(ctx, &svc)
|
||||
}
|
||||
164
internal/appliance/healthchecker/health_checker_test.go
Normal file
164
internal/appliance/healthchecker/health_checker_test.go
Normal file
@ -0,0 +1,164 @@
|
||||
package healthchecker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
"github.com/sourcegraph/log/logr"
|
||||
"github.com/sourcegraph/log/logtest"
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/k8senvtest"
|
||||
"github.com/sourcegraph/sourcegraph/internal/k8s/resource/service"
|
||||
)
|
||||
|
||||
var (
|
||||
// set once, before suite runs. See TestMain
|
||||
ctx context.Context
|
||||
k8sClient client.Client
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
logger := log.Scoped("appliance-healthchecker-tests")
|
||||
k8sConfig, cleanup, err := k8senvtest.SetupEnvtest(ctx, logr.New(logger), k8senvtest.NewNoopReconciler)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
if err := cleanup(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
k8sClient, err = client.New(k8sConfig, client.Options{})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rc := m.Run()
|
||||
|
||||
// Our earlier defer won't run after we call os.Exit() below
|
||||
if err := cleanup(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(rc)
|
||||
}
|
||||
|
||||
// A bit of a lengthy scenario-style test
|
||||
func TestManageIngressFacingService(t *testing.T) {
|
||||
ns, err := k8senvtest.NewRandomNamespace("test-appliance-self-update")
|
||||
require.NoError(t, err)
|
||||
err = k8sClient.Create(ctx, ns)
|
||||
require.NoError(t, err)
|
||||
|
||||
serviceName := types.NamespacedName{Namespace: ns.GetName(), Name: "sourcegraph-frontend"}
|
||||
checker := &HealthChecker{
|
||||
Probe: &PodProbe{K8sClient: k8sClient},
|
||||
K8sClient: k8sClient,
|
||||
Logger: logtest.Scoped(t),
|
||||
|
||||
ServiceName: serviceName,
|
||||
Graceperiod: 0,
|
||||
}
|
||||
|
||||
// Simulate helm having created the service, but no frontend pods have been
|
||||
// created yet
|
||||
svc := service.NewService("sourcegraph-frontend", ns.GetName(), nil)
|
||||
svc.Spec.Ports = []corev1.ServicePort{
|
||||
{Name: "http", Port: 30080, TargetPort: intstr.FromString("http")},
|
||||
}
|
||||
svc.Spec.Selector = map[string]string{
|
||||
"app": "sourcegraph-appliance-frontend",
|
||||
}
|
||||
err = k8sClient.Create(ctx, &svc)
|
||||
require.NoError(t, err)
|
||||
runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-appliance-frontend")
|
||||
|
||||
// Simulate some frontend pods existing but with no readiness conditions.
|
||||
pod1 := mkPod("pod1", ns.GetName())
|
||||
err = k8sClient.Create(ctx, pod1)
|
||||
require.NoError(t, err)
|
||||
pod2 := mkPod("pod2", ns.GetName())
|
||||
err = k8sClient.Create(ctx, pod2)
|
||||
require.NoError(t, err)
|
||||
runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-appliance-frontend")
|
||||
|
||||
// Simulate one pod becoming ready to receive traffic
|
||||
pod1.Status.Conditions = []corev1.PodCondition{
|
||||
{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionTrue,
|
||||
},
|
||||
}
|
||||
err = k8sClient.Status().Update(ctx, pod1)
|
||||
require.NoError(t, err)
|
||||
pod2.Status.Conditions = []corev1.PodCondition{
|
||||
{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionFalse,
|
||||
},
|
||||
}
|
||||
err = k8sClient.Status().Update(ctx, pod2)
|
||||
require.NoError(t, err)
|
||||
runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-frontend")
|
||||
|
||||
// test idempotency of the monitor
|
||||
runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-frontend")
|
||||
|
||||
// Simulate pods becoming unready
|
||||
pod1.Status.Conditions = []corev1.PodCondition{
|
||||
{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionFalse,
|
||||
},
|
||||
}
|
||||
err = k8sClient.Status().Update(ctx, pod1)
|
||||
require.NoError(t, err)
|
||||
runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-appliance-frontend")
|
||||
}
|
||||
|
||||
func runHealthCheckAndAssertSelector(t *testing.T, checker *HealthChecker, serviceName types.NamespacedName, namespace, expectedSelectorValue string) {
|
||||
err := checker.maybeFlipServiceOnce(ctx, "app=sourcegraph-frontend", namespace)
|
||||
require.NoError(t, err)
|
||||
|
||||
var svc corev1.Service
|
||||
err = k8sClient.Get(ctx, serviceName, &svc)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedSelectorValue, svc.Spec.Selector["app"])
|
||||
}
|
||||
|
||||
func mkPod(name, namespace string) *corev1.Pod {
|
||||
ctr := corev1.Container{
|
||||
Name: "frontend",
|
||||
Image: "foo:bar",
|
||||
Command: []string{"doitnow"},
|
||||
}
|
||||
return &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{"app": "sourcegraph-frontend"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{ctr},
|
||||
},
|
||||
}
|
||||
}
|
||||
38
internal/appliance/healthchecker/probe.go
Normal file
38
internal/appliance/healthchecker/probe.go
Normal file
@ -0,0 +1,38 @@
|
||||
package healthchecker
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
type PodProbe struct {
|
||||
K8sClient client.Client
|
||||
}
|
||||
|
||||
func (p *PodProbe) CheckPods(ctx context.Context, labelSelector, namespace string) error {
|
||||
var pods corev1.PodList
|
||||
selector, err := labels.Parse(labelSelector)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parsing label selector")
|
||||
}
|
||||
if err := p.K8sClient.List(ctx, &pods, &client.ListOptions{LabelSelector: selector, Namespace: namespace}); err != nil {
|
||||
return errors.Wrap(err, "listing pods")
|
||||
}
|
||||
for _, pod := range pods.Items {
|
||||
for _, condition := range pod.Status.Conditions {
|
||||
if condition.Type == corev1.PodReady {
|
||||
if condition.Status == corev1.ConditionTrue {
|
||||
// Return no error if even a single pod is ready
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("no pods are ready")
|
||||
}
|
||||
@ -2,150 +2,12 @@ package appliance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/life4/genesis/slices"
|
||||
passwordvalidator "github.com/wagslane/go-password-validator"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/config"
|
||||
"github.com/sourcegraph/sourcegraph/internal/releaseregistry"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
formValueOn = "on"
|
||||
)
|
||||
|
||||
func templatePath(name string) string {
|
||||
return filepath.Join("web", "template", name+".gohtml")
|
||||
}
|
||||
|
||||
func (a *Appliance) applianceHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ok, _ := a.shouldSetupRun(context.Background()); ok {
|
||||
http.Redirect(w, r, "/appliance/setup", http.StatusSeeOther)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func renderTemplate(name string, w io.Writer, data any) error {
|
||||
tmpl, err := template.ParseFS(templateFS, templatePath("layout"), templatePath(name))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "rendering template: %s", name)
|
||||
}
|
||||
return tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
func (a *Appliance) getSetupHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
versions, err := a.getVersions(r.Context())
|
||||
if err != nil {
|
||||
a.handleError(w, err, "getting versions")
|
||||
return
|
||||
}
|
||||
versions, err = NMinorVersions(versions, a.latestSupportedVersion, 2)
|
||||
if err != nil {
|
||||
a.handleError(w, err, "filtering versions to 2 minor points")
|
||||
return
|
||||
}
|
||||
|
||||
err = renderTemplate("setup", w, struct {
|
||||
Versions []string
|
||||
}{
|
||||
Versions: versions,
|
||||
})
|
||||
if err != nil {
|
||||
a.handleError(w, err, "executing template")
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Appliance) getLoginHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(a.adminPasswordBcrypt) == 0 {
|
||||
msg := fmt.Sprintf(
|
||||
"You must set a password: please create a secret named '%s' with key '%s'.",
|
||||
initialPasswordSecretName,
|
||||
initialPasswordSecretPasswordKey,
|
||||
)
|
||||
a.redirectToErrorPage(w, r, msg, errors.New("no admin password set"), true)
|
||||
return
|
||||
}
|
||||
|
||||
if err := renderTemplate("landing", w, struct {
|
||||
Flash string
|
||||
}{
|
||||
Flash: r.URL.Query().Get(queryKeyUserMessage),
|
||||
}); err != nil {
|
||||
a.handleError(w, err, "executing template")
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Appliance) postLoginHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userSuppliedPassword := r.FormValue("password")
|
||||
if err := bcrypt.CompareHashAndPassword(a.adminPasswordBcrypt, []byte(userSuppliedPassword)); err != nil {
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
a.redirectWithError(w, r, r.URL.Path, "Supplied password is incorrect.", err, true)
|
||||
return
|
||||
}
|
||||
|
||||
a.redirectToErrorPage(w, r, errMsgSomethingWentWrong, err, false)
|
||||
return
|
||||
}
|
||||
|
||||
if err := passwordvalidator.Validate(userSuppliedPassword, 60); err != nil {
|
||||
msg := fmt.Sprintf(
|
||||
"Please set a stronger password: delete the '%s' secret, and create a new secret named '%s' with key '%s'.",
|
||||
dataSecretName,
|
||||
initialPasswordSecretName,
|
||||
initialPasswordSecretPasswordKey,
|
||||
)
|
||||
a.redirectToErrorPage(w, r, msg, err, true)
|
||||
return
|
||||
}
|
||||
|
||||
validUntil := time.Now().Add(time.Hour).UTC()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
jwtClaimsValidUntilKey: validUntil.Format(time.RFC3339),
|
||||
})
|
||||
tokenStr, err := token.SignedString(a.jwtSecret)
|
||||
if err != nil {
|
||||
a.handleError(w, err, errMsgSomethingWentWrong)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: authCookieName,
|
||||
Value: tokenStr,
|
||||
Expires: validUntil,
|
||||
})
|
||||
http.Redirect(w, r, "/appliance", http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Appliance) handleError(w http.ResponseWriter, err error, msg string) {
|
||||
a.logger.Error(msg, log.Error(err))
|
||||
|
||||
// TODO we should probably look twice at this and decide whether it's in
|
||||
// line with existing standards.
|
||||
// Don't leak details of internal errors to users - that's why we have
|
||||
// logging above.
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintln(w, errMsgSomethingWentWrong)
|
||||
}
|
||||
|
||||
func (a *Appliance) getVersions(ctx context.Context) ([]string, error) {
|
||||
versions, err := a.releaseRegistryClient.ListVersions(ctx, "sourcegraph")
|
||||
if err != nil {
|
||||
@ -155,52 +17,3 @@ func (a *Appliance) getVersions(ctx context.Context) ([]string, error) {
|
||||
return version.Version, version.Public
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (a *Appliance) postSetupHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
a.logger.Error("failed to parse http form request", log.Error(err))
|
||||
// Handle err
|
||||
}
|
||||
|
||||
a.sourcegraph.Spec.RequestedVersion = r.FormValue("version")
|
||||
if r.FormValue("external_database") == formValueOn {
|
||||
a.sourcegraph.Spec.PGSQL.DatabaseConnection = &config.DatabaseConnectionSpec{
|
||||
Host: r.FormValue("pgsqlDBHost"),
|
||||
Port: r.FormValue("pgsqlDBPort"),
|
||||
User: r.FormValue("pgsqlDBUser"),
|
||||
Password: r.FormValue("pgsqlDBPassword"),
|
||||
Database: r.FormValue("pgsqlDBName"),
|
||||
}
|
||||
a.sourcegraph.Spec.CodeIntel.DatabaseConnection = &config.DatabaseConnectionSpec{
|
||||
Host: r.FormValue("codeintelDBHost"),
|
||||
Port: r.FormValue("codeintelDBPort"),
|
||||
User: r.FormValue("codeintelDBUser"),
|
||||
Password: r.FormValue("codeintelDBPassword"),
|
||||
Database: r.FormValue("codeintelDBName"),
|
||||
}
|
||||
a.sourcegraph.Spec.CodeInsights.DatabaseConnection = &config.DatabaseConnectionSpec{
|
||||
Host: r.FormValue("codeinsightsDBHost"),
|
||||
Port: r.FormValue("codeinsightsDBPort"),
|
||||
User: r.FormValue("codeinsightsDBUser"),
|
||||
Password: r.FormValue("codeinsightsDBPassword"),
|
||||
Database: r.FormValue("codeinsightsDBName"),
|
||||
}
|
||||
}
|
||||
// TODO validate user input
|
||||
|
||||
if r.FormValue("dev_mode") == formValueOn {
|
||||
a.sourcegraph.SetLocalDevMode()
|
||||
}
|
||||
|
||||
_, err = a.CreateConfigMap(r.Context(), "sourcegraph-appliance")
|
||||
if err != nil {
|
||||
a.logger.Error("failed to create configMap sourcegraph-appliance", log.Error(err))
|
||||
// Handle err
|
||||
}
|
||||
a.status = StatusInstalling
|
||||
|
||||
http.Redirect(w, r, "/appliance", http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
|
||||
213
internal/appliance/json.go
Normal file
213
internal/appliance/json.go
Normal file
@ -0,0 +1,213 @@
|
||||
package appliance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/config"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
const maxBytes = 1_048_576
|
||||
|
||||
type responseData map[string]any
|
||||
|
||||
func (a *Appliance) writeJSON(w http.ResponseWriter, status int, data responseData, headers http.Header) error {
|
||||
js, err := json.MarshalIndent(data, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
js = append(js, '\n')
|
||||
|
||||
for key, value := range headers {
|
||||
w.Header()[key] = value
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_, err = w.Write(js)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Appliance) readJSON(w http.ResponseWriter, r *http.Request, output any) error {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
|
||||
err := decoder.Decode(output)
|
||||
if err != nil {
|
||||
var jsonMaxBytesErrorType *http.MaxBytesError
|
||||
var jsonSyntaxErrorType *json.SyntaxError
|
||||
var jsonUnmarshalErrorType *json.UnmarshalTypeError
|
||||
var jsonInvalidUnmarshalErrorType *json.InvalidUnmarshalError
|
||||
|
||||
// list of de-facto errors common to JSON APIs that we want to wrap and handle
|
||||
switch {
|
||||
case strings.HasPrefix(err.Error(), "json: unknown field"):
|
||||
return errors.Newf("request body contains unknown key")
|
||||
|
||||
case errors.Is(err, io.EOF):
|
||||
return errors.New("request body must not be empty")
|
||||
|
||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
||||
return errors.New("malformed JSON contained in request body")
|
||||
|
||||
case errors.As(err, &jsonSyntaxErrorType):
|
||||
return errors.Newf("malformed JSON found at character %d", jsonSyntaxErrorType.Offset)
|
||||
|
||||
case errors.As(err, &jsonMaxBytesErrorType):
|
||||
return errors.Newf("request body larger than %d bytes", jsonMaxBytesErrorType.Limit)
|
||||
|
||||
case errors.As(err, &jsonUnmarshalErrorType):
|
||||
if jsonUnmarshalErrorType.Field != "" {
|
||||
return errors.Newf("incorrect JSON type for field %q", jsonUnmarshalErrorType.Field)
|
||||
}
|
||||
return errors.Newf("incorrect JSON type found at character %d", jsonUnmarshalErrorType.Offset)
|
||||
|
||||
case errors.As(err, &jsonInvalidUnmarshalErrorType):
|
||||
panic(err)
|
||||
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = decoder.Decode(&struct{}{})
|
||||
if !errors.Is(err, io.EOF) {
|
||||
return errors.New("request body must only contain single JSON value")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Appliance) getStatusJSONHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := struct {
|
||||
Status string `json:"status"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}{
|
||||
Status: a.status.String(),
|
||||
Data: "",
|
||||
}
|
||||
|
||||
if err := a.writeJSON(w, http.StatusOK, responseData{"status": data}, http.Header{}); err != nil {
|
||||
a.serverErrorResponse(w, r, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Appliance) getInstallProgressJSONHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
currentTasks, progress := calculateProgress(installTasks())
|
||||
|
||||
installProgress := struct {
|
||||
Version string `json:"version"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
Tasks []Task `json:"tasks"`
|
||||
}{
|
||||
Version: "",
|
||||
Progress: progress,
|
||||
Error: "",
|
||||
Tasks: currentTasks,
|
||||
}
|
||||
|
||||
ok, err := a.isSourcegraphFrontendReady(r.Context())
|
||||
if err != nil {
|
||||
a.logger.Error("failed to get sourcegraph frontend status")
|
||||
return
|
||||
}
|
||||
|
||||
if ok {
|
||||
a.status = config.StatusWaitingForAdmin
|
||||
}
|
||||
|
||||
if err := a.writeJSON(w, http.StatusOK, responseData{"progress": installProgress}, http.Header{}); err != nil {
|
||||
a.serverErrorResponse(w, r, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Appliance) getMaintenanceStatusHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type service struct {
|
||||
Name string `json:"name"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
services := []service{}
|
||||
for _, name := range config.SourcegraphServicesToReconcile {
|
||||
services = append(services, service{
|
||||
Name: name,
|
||||
Healthy: true,
|
||||
Message: "fake event",
|
||||
})
|
||||
}
|
||||
fmt.Println(services)
|
||||
if err := a.writeJSON(w, http.StatusOK, responseData{"services": services}, http.Header{}); err != nil {
|
||||
a.serverErrorResponse(w, r, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Appliance) postStatusJSONHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var input struct {
|
||||
State string `json:"state"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
if err := a.readJSON(w, r, &input); err != nil {
|
||||
a.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
newStatus := config.Status(input.State)
|
||||
a.logger.Info("state transition", log.String("state", string(newStatus)))
|
||||
// trim v if v exists
|
||||
input.Data = strings.TrimPrefix(input.Data, "v")
|
||||
a.sourcegraph.Spec.RequestedVersion = input.Data
|
||||
if err := a.setStatus(r.Context(), newStatus); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
a.logger.Info("no configmap found, will not set status")
|
||||
} else {
|
||||
a.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if a.noResourceRestrictions {
|
||||
a.sourcegraph.SetLocalDevMode()
|
||||
}
|
||||
|
||||
cfgMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "sourcegraph-appliance",
|
||||
Namespace: a.namespace,
|
||||
},
|
||||
}
|
||||
err := a.reconcileConfigMap(r.Context(), cfgMap)
|
||||
if err != nil {
|
||||
a.serverErrorResponse(w, r, err)
|
||||
}
|
||||
|
||||
a.status = newStatus
|
||||
})
|
||||
}
|
||||
230
internal/appliance/json_test.go
Normal file
230
internal/appliance/json_test.go
Normal file
@ -0,0 +1,230 @@
|
||||
package appliance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/sourcegraph/log"
|
||||
)
|
||||
|
||||
func TestReadJSON(t *testing.T) {
|
||||
appliance := &Appliance{
|
||||
logger: log.NoOp(),
|
||||
}
|
||||
|
||||
t.Run("ValidJSON", func(t *testing.T) {
|
||||
body := `{"key": "value"}`
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
var output map[string]string
|
||||
err := appliance.readJSON(w, req, &output)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(map[string]string{"key": "value"}, output); diff != "" {
|
||||
t.Errorf("output mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyBody", func(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
var output map[string]string
|
||||
err := appliance.readJSON(w, req, &output)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
} else if err.Error() != "request body must not be empty" {
|
||||
t.Errorf("unexpected error message: got %q, want %q", err.Error(), "request body must not be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MalformedJSON", func(t *testing.T) {
|
||||
body := `{"key": "value",}`
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
var output map[string]string
|
||||
err := appliance.readJSON(w, req, &output)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
} else if !strings.HasPrefix(err.Error(), "malformed JSON found at character") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UnknownField", func(t *testing.T) {
|
||||
body := `{"unknown_field": "value"}`
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
var output struct{}
|
||||
err := appliance.readJSON(w, req, &output)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
} else if err.Error() != "request body contains unknown key" {
|
||||
t.Errorf("unexpected error message: got %q, want %q", err.Error(), "request body contains unknown key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IncorrectJSONType", func(t *testing.T) {
|
||||
body := `{"key": 123}`
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
var output struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
err := appliance.readJSON(w, req, &output)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
} else if err.Error() != `incorrect JSON type for field "key"` {
|
||||
t.Errorf("unexpected error message: got %q, want %q", err.Error(), `incorrect JSON type for field "key"`)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MultipleJSONValues", func(t *testing.T) {
|
||||
body := `{"key1": "value1"}{"key2": "value2"}`
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
var output map[string]string
|
||||
err := appliance.readJSON(w, req, &output)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
} else if err.Error() != "request body must only contain single JSON value" {
|
||||
t.Errorf("unexpected error message: got %q, want %q", err.Error(), "request body must only contain single JSON value")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LargeBody", func(t *testing.T) {
|
||||
// Create a large JSON object
|
||||
largeObject := map[string]string{}
|
||||
for i := 0; i < maxBytes/10; i++ {
|
||||
key := fmt.Sprintf("key%d", i)
|
||||
largeObject[key] = strings.Repeat("a", 10)
|
||||
}
|
||||
|
||||
largeJSON, _ := json.Marshal(largeObject)
|
||||
// Ensure the JSON is larger than maxBytes
|
||||
largeJSON = append(largeJSON, []byte(`,"extra":"data"}`)...)
|
||||
|
||||
req := httptest.NewRequest("POST", "/", bytes.NewReader(largeJSON))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
var output map[string]string
|
||||
err := appliance.readJSON(w, req, &output)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
} else if !strings.HasPrefix(err.Error(), "request body larger than") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteJSON(t *testing.T) {
|
||||
appliance := &Appliance{
|
||||
logger: log.NoOp(),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
status int
|
||||
data responseData
|
||||
headers http.Header
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Simple JSON response",
|
||||
status: http.StatusOK,
|
||||
data: responseData{
|
||||
"message": "Hello, World!",
|
||||
},
|
||||
headers: nil,
|
||||
expected: "{\n\t\"message\": \"Hello, World!\"\n}\n",
|
||||
},
|
||||
{
|
||||
name: "JSON response with custom headers",
|
||||
status: http.StatusCreated,
|
||||
data: responseData{
|
||||
"id": 1,
|
||||
"name": "Test",
|
||||
},
|
||||
headers: http.Header{
|
||||
"X-Custom-Header": []string{"CustomValue"},
|
||||
},
|
||||
expected: "{\n\t\"id\": 1,\n\t\"name\": \"Test\"\n}\n",
|
||||
},
|
||||
{
|
||||
name: "Empty JSON response",
|
||||
status: http.StatusNoContent,
|
||||
data: responseData{},
|
||||
headers: nil,
|
||||
expected: "{}\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
err := appliance.writeJSON(w, tt.status, tt.data, tt.headers)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.status, w.Code); diff != "" {
|
||||
t.Errorf("status mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff("application/json", w.Header().Get("Content-Type")); diff != "" {
|
||||
t.Errorf("Content-Type mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, w.Body.String()); diff != "" {
|
||||
t.Errorf("body mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if tt.headers != nil {
|
||||
for key, value := range tt.headers {
|
||||
if diff := cmp.Diff(value, w.Header()[key]); diff != "" {
|
||||
t.Errorf("header %q mismatch (-want +got):\n%s", key, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteJSONError(t *testing.T) {
|
||||
appliance := &Appliance{
|
||||
logger: log.NoOp(),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
data := responseData{
|
||||
"data": make(chan int),
|
||||
}
|
||||
|
||||
err := appliance.writeJSON(w, http.StatusOK, data, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
}
|
||||
|
||||
expectedErrSubstring := "json: unsupported type: chan int"
|
||||
if diff := cmp.Diff(true, strings.Contains(err.Error(), expectedErrSubstring)); diff != "" {
|
||||
t.Errorf("error message mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@ -2,13 +2,18 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "k8senvtest",
|
||||
srcs = ["envtest.go"],
|
||||
srcs = [
|
||||
"envtest.go",
|
||||
"namespaces.go",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/k8senvtest",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//lib/errors",
|
||||
"@com_github_go_logr_logr//:logr",
|
||||
"@io_bazel_rules_go//go/runfiles:go_default_library",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||
"@io_k8s_client_go//kubernetes/scheme",
|
||||
"@io_k8s_client_go//rest",
|
||||
"@io_k8s_sigs_controller_runtime//:controller-runtime",
|
||||
|
||||
17
internal/appliance/k8senvtest/README.md
Normal file
17
internal/appliance/k8senvtest/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# k8senvtest
|
||||
|
||||
A wrapper package for sigs.k8s.io/controller-runtime/pkg/envtest. Has
|
||||
compatibility with our bazel setup. Any package that makes us of this one should
|
||||
add the following to the go_test directive in its BUILD.bazel:
|
||||
|
||||
```starlark
|
||||
data = [
|
||||
"//dev/tools:kubebuilder-assets",
|
||||
],
|
||||
env = {
|
||||
"KUBEBUILDER_ASSET_PATHS": "$(rlocationpaths //dev/tools:kubebuilder-assets)",
|
||||
},
|
||||
```
|
||||
|
||||
And this should just work out of the box. See consumers of this package for
|
||||
examples on how to use it, including safe teardown.
|
||||
@ -119,3 +119,11 @@ func kubebuilderAssetPathLocalDev() (string, error) {
|
||||
}
|
||||
return strings.TrimSpace(envtestOut.String()), nil
|
||||
}
|
||||
|
||||
func NewNoopReconciler(mgr ctrl.Manager) KubernetesController {
|
||||
return noopReconicler{}
|
||||
}
|
||||
|
||||
type noopReconicler struct{}
|
||||
|
||||
func (noopReconicler) SetupWithManager(_ ctrl.Manager) error { return nil }
|
||||
|
||||
34
internal/appliance/k8senvtest/namespaces.go
Normal file
34
internal/appliance/k8senvtest/namespaces.go
Normal file
@ -0,0 +1,34 @@
|
||||
package k8senvtest
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// In order to be able to run tests in isolation, we can make use of namespaces
|
||||
// with a random suffix. We don't need to delete these, all data will be
|
||||
// desstroyed on envtest teardown.
|
||||
func NewRandomNamespace(prefix string) (*corev1.Namespace, error) {
|
||||
slug, err := randomSlug()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := fmt.Sprintf("%s-%s", prefix, slug)
|
||||
return &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func randomSlug() (string, error) {
|
||||
buf := make([]byte, 3)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
# Operator Maintenance UI
|
||||
|
||||
## Components
|
||||
|
||||
This project contains the following components:
|
||||
|
||||
### Maintenance UI
|
||||
|
||||
A React + Material UI application that communicates with the Operator and gathers data and display status.
|
||||
|
||||
Features:
|
||||
|
||||
- Installation
|
||||
- Health & Actions
|
||||
- Upgrade
|
||||
|
||||
### Mock Operator API
|
||||
|
||||
In the [mock-api](./mock-api/) folder, a Go Server application that implements the Operator API companion to the Maintenance UI.
|
||||
|
||||
#### Mock Operator Debug Bar API
|
||||
|
||||
We also implement some test APIs to enable controlling the Mock Operator from the Maitenance UI.
|
||||
|
||||
## Running Locally (Developer Mode)
|
||||
|
||||
1. Run the go application in the `mock-api` folder:
|
||||
|
||||
```
|
||||
$ cd mock-api
|
||||
$ go run ./cmd
|
||||
```
|
||||
|
||||
2. Run the Maitenance UI:
|
||||
|
||||
```
|
||||
$ pnpm run dev
|
||||
```
|
||||
|
||||
## Building Images
|
||||
|
||||
```
|
||||
$ cd build
|
||||
$ make
|
||||
```
|
||||
|
||||
It will:
|
||||
|
||||
1. Build frontend and backend distributables
|
||||
2. Build docker images
|
||||
3. Push images to the container registry
|
||||
4. Update the Helm chart with the appropriate registry image versions
|
||||
|
||||
## Helm Chart
|
||||
|
||||
### Preparing the Helm Chart
|
||||
|
||||
No action. This step is automated by the image build step.
|
||||
|
||||
### Packaging the Helm Chart
|
||||
|
||||
TBD
|
||||
|
||||
### Installing the Helm Chart
|
||||
|
||||
1. Have a Kubernetes cluster configured and available at the command line
|
||||
2. Test you can access the cluster by running: `kubectl get pods`
|
||||
3. Install the Helm chart:
|
||||
|
||||
```
|
||||
$ helm install operator ./helm
|
||||
```
|
||||
|
||||
Installer will create the `sourcegraph` namespace
|
||||
|
||||
4. Execute the commands output by the installer to get the address of
|
||||
the maintenance UI
|
||||
|
||||
### Launching the Maintenance UI
|
||||
|
||||
Once the data provided by the install step is available,
|
||||
IP address + maintenance password, open the maintenance UI in your
|
||||
browser and follow along the wizard.
|
||||
|
||||
### Run debug console
|
||||
|
||||
Maintenance UI has a debug console that can be used to control flows in the maintenance UI,
|
||||
to enable set `debugbar: true` in your browser local storage.
|
||||
@ -13,8 +13,5 @@ go_library(
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/api",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/appliance/maintenance/backend/operator",
|
||||
"@com_github_gorilla_mux//:mux",
|
||||
],
|
||||
deps = ["@com_github_gorilla_mux//:mux"],
|
||||
)
|
||||
|
||||
@ -3,19 +3,17 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/operator"
|
||||
)
|
||||
|
||||
var installError string = ""
|
||||
var installTasks []operator.Task = createInstallTasks()
|
||||
var installTasks []Task = createInstallTasks()
|
||||
var installVersion string = ""
|
||||
|
||||
type InstallProgress struct {
|
||||
Version string `json:"version"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
Tasks []operator.Task `json:"tasks"`
|
||||
Version string `json:"version"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
Tasks []Task `json:"tasks"`
|
||||
}
|
||||
|
||||
func InstallProgressHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -8,8 +8,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/operator"
|
||||
)
|
||||
|
||||
var maintenanceEndpoint = os.Getenv("MAINTENANCE_ENDPOINT")
|
||||
@ -21,6 +19,26 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
type status struct {
|
||||
Stage Stage `json:"stage"`
|
||||
CurrentVersion *string `json:"version"` // current version, nil if not installed
|
||||
NextVersion *string `json:"nextVersion"` // version being installed/upgraded nil if not being installed/upgraded
|
||||
Tasks []Task `json:"tasks"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
type Stage string
|
||||
|
||||
const (
|
||||
StageUnknown Stage = "unknown"
|
||||
StageIdle Stage = "idle"
|
||||
StageInstall Stage = "install"
|
||||
StageInstalling Stage = "installing"
|
||||
StageUpgrading Stage = "upgrading"
|
||||
StageWaitingForAdmin Stage = "wait-for-admin"
|
||||
StageRefresh Stage = "refresh"
|
||||
)
|
||||
|
||||
type Feature struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
@ -40,7 +58,7 @@ type StageResponse struct {
|
||||
|
||||
var epoch = time.Unix(0, 0)
|
||||
|
||||
var currentStage operator.Stage = operator.StageInstall
|
||||
var currentStage Stage = StageInstall
|
||||
var switchToAdminTime time.Time = epoch
|
||||
|
||||
func init() {
|
||||
@ -55,17 +73,17 @@ func StageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch status {
|
||||
case "installing":
|
||||
currentStage = operator.StageInstalling
|
||||
currentStage = StageInstalling
|
||||
case "ready":
|
||||
fmt.Println("ready!", switchToAdminTime, currentStage)
|
||||
if switchToAdminTime == time.Unix(0, 0) {
|
||||
if currentStage != operator.StageRefresh && currentStage != operator.StageWaitingForAdmin {
|
||||
if currentStage != StageRefresh && currentStage != StageWaitingForAdmin {
|
||||
switchToAdminTime = time.Now().Add(5 * time.Second)
|
||||
}
|
||||
} else {
|
||||
if time.Now().After(switchToAdminTime) {
|
||||
switchToAdminTime = epoch
|
||||
currentStage = operator.StageWaitingForAdmin
|
||||
currentStage = StageWaitingForAdmin
|
||||
}
|
||||
}
|
||||
case "unknown":
|
||||
@ -77,8 +95,8 @@ func StageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
switch currentStage {
|
||||
case operator.StageRefresh:
|
||||
currentStage = operator.StageUnknown
|
||||
case StageRefresh:
|
||||
currentStage = StageUnknown
|
||||
}
|
||||
|
||||
fmt.Println("Sending current stage", result)
|
||||
@ -92,12 +110,12 @@ func SetStageHandlerForTesting(w http.ResponseWriter, r *http.Request) {
|
||||
receiveJson(w, r, &request)
|
||||
|
||||
fmt.Println("Setting stage to", request.Stage)
|
||||
currentStage = operator.Stage(request.Stage)
|
||||
currentStage = Stage(request.Stage)
|
||||
|
||||
fmt.Println(installTasks)
|
||||
|
||||
switch currentStage {
|
||||
case operator.StageInstalling:
|
||||
case StageInstalling:
|
||||
installError = ""
|
||||
installTasks = createInstallTasks()
|
||||
installVersion = request.Data
|
||||
@ -107,7 +125,7 @@ func SetStageHandlerForTesting(w http.ResponseWriter, r *http.Request) {
|
||||
installError = err.Error()
|
||||
}
|
||||
}()
|
||||
case operator.StageUpgrading:
|
||||
case StageUpgrading:
|
||||
installError = ""
|
||||
installTasks = createFakeUpgradeTasks()
|
||||
installVersion = request.Data
|
||||
|
||||
@ -3,16 +3,24 @@ package api
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/operator"
|
||||
)
|
||||
|
||||
const InstallTaskWaitForCluster = 0
|
||||
const InstallTaskSetup = 1
|
||||
const InstallTaskStart = 2
|
||||
|
||||
func createInstallTasks() []operator.Task {
|
||||
return []operator.Task{
|
||||
type Task struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Started bool `json:"started"`
|
||||
Finished bool `json:"finished"`
|
||||
Weight int `json:"weight"`
|
||||
Progress int `json:"progress"`
|
||||
LastUpdate time.Time `json:"lastUpdate"`
|
||||
}
|
||||
|
||||
func createInstallTasks() []Task {
|
||||
return []Task{
|
||||
{
|
||||
Title: "Warming up",
|
||||
Description: "Setting up basic resources",
|
||||
@ -37,8 +45,8 @@ func createInstallTasks() []operator.Task {
|
||||
}
|
||||
}
|
||||
|
||||
func createFakeUpgradeTasks() []operator.Task {
|
||||
return []operator.Task{
|
||||
func createFakeUpgradeTasks() []Task {
|
||||
return []Task{
|
||||
{
|
||||
Title: "Upgrade",
|
||||
Description: "Upgrade Sourcegraph",
|
||||
@ -56,8 +64,8 @@ func createFakeUpgradeTasks() []operator.Task {
|
||||
}
|
||||
}
|
||||
|
||||
func progressTasks(tasks []operator.Task) []operator.Task {
|
||||
var result []operator.Task
|
||||
func progressTasks(tasks []Task) []Task {
|
||||
var result []Task
|
||||
|
||||
var previousStarted bool = true
|
||||
var previousFinished bool = true
|
||||
@ -76,8 +84,8 @@ func progressTasks(tasks []operator.Task) []operator.Task {
|
||||
return result
|
||||
}
|
||||
|
||||
func calculateProgress() ([]operator.Task, int) {
|
||||
var result []operator.Task
|
||||
func calculateProgress() ([]Task, int) {
|
||||
var result []Task
|
||||
|
||||
var taskWeights int = 0
|
||||
for _, t := range installTasks {
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "cmd_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/cmd",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["//internal/appliance/maintenance/backend/api"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "cmd",
|
||||
embed = [":cmd_lib"],
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
@ -1,13 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := api.New()
|
||||
fmt.Println("Starting mock API server")
|
||||
server.Run()
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "operator",
|
||||
srcs = [
|
||||
"manage.go",
|
||||
"task.go",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/operator",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
@ -1,58 +0,0 @@
|
||||
package operator
|
||||
|
||||
type K8sManager interface {
|
||||
Status() *status
|
||||
Install(version string) error
|
||||
Upgrade(version string) error
|
||||
}
|
||||
|
||||
func New() K8sManager {
|
||||
return &manager{}
|
||||
}
|
||||
|
||||
type status struct {
|
||||
Stage Stage `json:"stage"`
|
||||
CurrentVersion *string `json:"version"` // current version, nil if not installed
|
||||
NextVersion *string `json:"nextVersion"` // version being installed/upgraded nil if not being installed/upgraded
|
||||
Tasks []Task `json:"tasks"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
type Stage string
|
||||
|
||||
const (
|
||||
StageUnknown Stage = "unknown"
|
||||
StageIdle Stage = "idle"
|
||||
StageInstall Stage = "install"
|
||||
StageInstalling Stage = "installing"
|
||||
StageUpgrading Stage = "upgrading"
|
||||
StageWaitingForAdmin Stage = "wait-for-admin"
|
||||
StageRefresh Stage = "refresh"
|
||||
)
|
||||
|
||||
type manager struct{}
|
||||
|
||||
// Asks the Operator to kick off a new installation of the specified version.
|
||||
//
|
||||
// Returns an error if the installation was not successful,
|
||||
// if the version is not supported, or a version is already installed.
|
||||
//
|
||||
// Once the request is accepted, the status can be tracked via the Status() method.
|
||||
func (*manager) Install(version string) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Asks the Operator to upgrade to the specified version.
|
||||
//
|
||||
// Returns an error if the upgrade was not successful,
|
||||
// if the version is not supported, or if there's no existing version installed.
|
||||
//
|
||||
// Once the request is accepted, the status can be tracked via the Status() method.
|
||||
func (*manager) Upgrade(version string) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Returns the current status of the Operator.
|
||||
func (*manager) Status() *status {
|
||||
panic("unimplemented")
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
package operator
|
||||
|
||||
import "time"
|
||||
|
||||
type Task struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Started bool `json:"started"`
|
||||
Finished bool `json:"finished"`
|
||||
Weight int `json:"weight"`
|
||||
Progress int `json:"progress"`
|
||||
LastUpdate time.Time `json:"lastUpdate"`
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
HTML,
|
||||
BODY {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
BODY {
|
||||
font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
|
||||
"Lucida Sans Unicode", Geneva, Verdana, sans-serif;
|
||||
}
|
||||
HEADER {
|
||||
display: flex;
|
||||
padding: 0.5rem 1rem;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: #666666;
|
||||
}
|
||||
IMG {
|
||||
height: 64px;
|
||||
}
|
||||
H1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: white;
|
||||
font-size: 3em;
|
||||
}
|
||||
H2 {
|
||||
border-top: 1px solid gray;
|
||||
border-left: 1px solid gray;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
.content {
|
||||
padding: 1rem;
|
||||
}
|
||||
PRE {
|
||||
background-color: #dddddd;
|
||||
padding: 1rem;
|
||||
display: inline-block;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.small {
|
||||
background-color: #dddddd;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
P {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
LI {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<img src="https://sourcegraph.com/sourcegraph-reverse-logo.svg" />
|
||||
<h1>Appliance</h1>
|
||||
</header>
|
||||
<div class="content">
|
||||
<h2>Pre-Requisites</h2>
|
||||
<ol>
|
||||
<li>A Kubernetes Cluster (any kind: k3s, minicube, GKE, EKS, etc)</li>
|
||||
<li>
|
||||
<pre class="small">kubectl</pre>
|
||||
configured in your command line with credentials to the cluster
|
||||
</li>
|
||||
<li>
|
||||
Kubernetes context set to the namespace you want to create
|
||||
Sourcegraph.
|
||||
<p>
|
||||
If you don't ever set, it will install in the
|
||||
<span class="small">default</span> namespace
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h2>Install</h2>
|
||||
<p>
|
||||
This is the only cluster piece required. From this point on, all
|
||||
installation happens guided by the Operator:
|
||||
</p>
|
||||
<pre>
|
||||
kubectl apply -f https://storage.googleapis.com/merge-appliance-demo/v0.0.5999925/bundle.yaml</pre
|
||||
>
|
||||
<p>
|
||||
We will need to get the IP address of the Appliance, as well the
|
||||
maintenance password.
|
||||
</p>
|
||||
<p>The steps below help you get those values...</p>
|
||||
|
||||
<h2>Get Frontend Address</h2>
|
||||
<pre>kubectl get svc operator-ui --watch</pre>
|
||||
<p>Once the external IP address is available, you visit that page.</p>
|
||||
|
||||
<pre>
|
||||
% kubectl get svc operator-ui
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
operator-ui LoadBalancer 10.92.6.197 34.71.130.103 80:31883/TCP 10h
|
||||
⇑⇑⇑⇑⇑⇑⇑⇑⇑⇑⇑⇑⇑
|
||||
this address</pre
|
||||
>
|
||||
|
||||
<h2>Navigate to the Appliance Page</h2>
|
||||
<pre>http://<ip-address-above>/</pre>
|
||||
|
||||
<h2>Get the Maintenance Password</h2>
|
||||
<pre>
|
||||
kubectl get secret operator-api -o json \
|
||||
| jq '{name: .metadata.name,data: .data|map_values(@base64d)}'</pre
|
||||
>
|
||||
|
||||
<p>Example output:</p>
|
||||
|
||||
<pre>
|
||||
{
|
||||
"name": "operator-api",
|
||||
"data": {
|
||||
"MAINTENANCE_PASSWORD": "password-is-here"
|
||||
}
|
||||
}</pre
|
||||
>
|
||||
|
||||
<h2>Install</h2>
|
||||
<ol>
|
||||
<li>Follow the wizard</li>
|
||||
<li>
|
||||
Once the installation is complete, you will see a "Wait for Admin to
|
||||
Return"
|
||||
<p>
|
||||
This step is to avoid exposing the admin UI before creating a user,
|
||||
allowing, for example, the administrator to leave the
|
||||
installation/upgrade/maintenance going and walk away from the
|
||||
computer.
|
||||
</p>
|
||||
</li>
|
||||
<li>Press the Launch button and the Admin UI will start</li>
|
||||
</ol>
|
||||
|
||||
<h2>Teardown</h2>
|
||||
<p>This will <b>DELETE ALL DATA:</b></p>
|
||||
<pre>
|
||||
kubectl delete -f https://storage.googleapis.com/merge-appliance-demo/v0.0.5999925/bundle.yaml
|
||||
kubectl delete pvc --all</pre
|
||||
>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,23 +0,0 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user