Compare commits

...

24 Commits

Author SHA1 Message Date
Warren Gifford
dc97541a28
cherry-pick 856e41e with resolved conflicts (#64228)
<!-- PR description tips:
https://www.notion.so/sourcegraph/Write-a-good-pull-request-description-610a7fd3e613496eb76f450db5a49b6e
-->
Cherry pick changes from
https://github.com/sourcegraph/sourcegraph/pull/64227 into release
branch

## Test plan
tested via sg start with both:
- export APPLIANCE_UPDATE_TARGET=http://www.google.com
- export APPLIANCE_MENU_TARGET=http://www.warhammer.com

<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->

## Changelog

<!-- OPTIONAL; info at
https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c
-->
2024-08-01 23:32:25 +00:00
Release Bot
e1e2029d29
[Backport 5.5.x] fix(appliance): reliably redirect to site-admin post-install (#64220)
Backport e54407d9f5 from #64216

Co-authored-by: Craig Furman <craig.furman@sourcegraph.com>
2024-08-01 17:06:28 +00:00
Release Bot
17871a4647
[Backport 5.5.x] fix(appliance): cache authorization status (#64219)
Backport 156aa5a0ad from #64213

Co-authored-by: Craig Furman <craig.furman@sourcegraph.com>
2024-08-01 16:57:47 +00:00
Craig Furman
d24e8fe7f3
feat(appliance): backport all recent appliance changes (#64182)
Draft in case plan in
https://linear.app/sourcegraph/issue/REL-309/release-process-for-appliance
not agreed. Please see that first.

Generated by:

```
git log --format=%H d47b4cc48b6ea27cf6b5a274b79a6a4c8f38cf8c..origin/main -- cmd/appliance internal/appliance docker-images/appliance-frontend | tac | xargs git cherry-pick
```

d47b4cc48b being the commit we branched
off main from to create the 5.5.x branch
(https://buildkite.com/sourcegraph/sourcegraph/builds/281882).

Commits (generated by `git log --format='-
https://github.com/sourcegraph/sourcegraph/commit/%H'
d47b4cc48b6ea27cf6b5a274b79a6a4c8f38cf8c..origin/main -- cmd/appliance
internal/appliance docker-images/appliance-frontend | tac`):

-
a20b0650b4
-
b71c986c77
-
91864283bc
-
c88b57020f
-
0491839942
-
619fc57074
-
e81c39a834
-
a61f353e0e
-
0abef7b43d
-
0e391a964a
-
daae9adfb6
-
6e31f0f4cc
-
49a600220d
-
37cf4a7b7e
-
29fc613c37
-
255e6387cc
-
49b32fcf3a
-
9f4c160f91
-
3814fd7390
-
c68e92bc28
-
7e82c27ab5
-
98c6b9703f
-
a01ebad841
-
8c2d8da234
-
ebec72d7ed
-
d945f19285
-
84e28998e9


## Test plan

Tests pass.

## Changelog

- Backport all recent appliance changes. The appliance is still
pre-release.

---------

Co-authored-by: Jacob Pleiness <jdpleiness@users.noreply.github.com>
Co-authored-by: Anish Lakhwara <anish+github@lakhwara.com>
Co-authored-by: Warren Gifford <warren@sourcegraph.com>
Co-authored-by: Nelson Araujo <nelsonjr@users.noreply.github.com>
2024-07-31 17:26:56 +00:00
Ara
162d3836da
Backport 5ce2eea to 5.5.x (#64166)
This is a backport PR to add changes from
https://github.com/sourcegraph/sourcegraph/pull/64116 to v5.5.x to main
to create a release of the frontend.


## Test plan

<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->

## Changelog

<!-- OPTIONAL; info at
https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c
-->

---------

Co-authored-by: Vincent <evict@users.noreply.github.com>
2024-07-31 09:19:19 -07:00
Release Bot
8cf3916c44
[Backport 5.5.x] fix: Fix Chrome stack overflow during highlighting (#64074)
Using the spread operator with large arrays can trigger a
stack overflow in Chrome/V8.

In a highlighting context, we can have 10k-100k occurrences
in a file, so let's avoid using the spread operator.

Fixes https://linear.app/sourcegraph/issue/GRAPH-772

## Test plan

Manually tested against sample file.

![CleanShot 2024-07-25 at 11 10 43@2x](https://github.com/user-attachments/assets/e096c664-063e-44ed-a991-72629af36651)

## Changelog

- Fixes a Chrome-specific stack overflow when highlighting large files.
 <br> Backport 2644e24244 from #64072

Co-authored-by: Varun Gandhi <varun.gandhi@sourcegraph.com>
2024-07-25 20:44:22 +08:00
Release Bot
72ab1f818b
[Backport 5.5.x] ci: make internal+promote release higher priority in runtypes (#64050)
With the https://github.com/sourcegraph/sourcegraph/pull/63985/files
PatchRelease is matched before InternalRelease leading to the wrong
build being generated.

We therefore move the Promote and Internal Release runtypes higher in
priority so that they get matched first.

## Test plan
```
export RELEASE_INTERNAL=true
export VERSION=&quot;5.5.2463&quot;
go run ./dev/sg ci preview
```
👇🏼 
```
go run ./dev/sg ci preview
⚠️ Running sg with a dev build, following flags have different default value unless explictly set: skip-auto-update, disable-analytics
If the current branch were to be pushed, the following pipeline would be run:
  Parsed diff:
  changed files: [WORKSPACE client/web-sveltekit/BUILD.bazel client/web-sveltekit/playwright.config.ts client/web-sveltekit/src/lib/navigation/GlobalHeader.svelte client/web-
  sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts client/web/src/cody/chat/new-chat/NewCodyChatPage.tsx client/web/src/cody/sidebar/new-cody-sidebar/NewCodySidebar.tsx
  client/web/src/cody/sidebar/new-cody-sidebar/NewCodySidebarWebChat.tsx client/web/src/enterprise/batches/settings/AddCredentialModal.tsx
  client/web/src/enterprise/batches/settings/BatchChangesCreateGitHubAppPage.tsx client/web/src/repo/blame/hooks.ts client/web/src/repo/blame/shared.ts cmd/frontend/auth/user.go
  cmd/frontend/auth/user_test.go cmd/frontend/internal/codycontext/context.go cmd/frontend/internal/codycontext/context_test.go deps.bzl dev/ci/push_all.sh dev/ci/runtype/runtype.go go.mod go.sum
  internal/codeintel/uploads/BUILD.bazel internal/codeintel/uploads/internal/background/backfiller/BUILD.bazel internal/codeintel/uploads/internal/background/backfiller/mocks_test.go
  internal/codeintel/uploads/internal/background/commitgraph/BUILD.bazel internal/codeintel/uploads/internal/background/commitgraph/job_commitgraph.go
  internal/codeintel/uploads/internal/background/expirer/BUILD.bazel internal/codeintel/uploads/internal/background/expirer/mocks_test.go
  internal/codeintel/uploads/internal/background/processor/BUILD.bazel internal/codeintel/uploads/internal/background/processor/mocks_test.go internal/codeintel/uploads/internal/store/BUILD.bazel
  internal/codeintel/uploads/internal/store/commitdate.go internal/codeintel/uploads/internal/store/commitdate_test.go internal/codeintel/uploads/internal/store/observability.go
  internal/codeintel/uploads/internal/store/store.go internal/codeintel/uploads/mocks_test.go internal/database/migration/shared/data/cmd/generator/consts.go
  internal/database/migration/shared/data/stitched-migration-graph.json package.json pnpm-lock.yaml schema/schema.go schema/site.schema.json]
  diff changes: &quot;Go, Client, pnpm, Docs, Shell&quot;
  The generated build pipeline will now follow, see you next time!

  • Detected run type: Internal release
  • Detected diffs: Go, Client, pnpm, Docs, Shell
  • Computed variables:
    • VERSION=5.5.2463
  • Computed build steps:
    • Aspect Workflow specific steps
      • 🤖 Generated steps that include Buildifier, Gazelle, Test and Integration/E2E tests
    • Image builds
      • :bazel::packer: 🚧 Build executor image
    • :bazel: Bazel prechecks &amp; build  sg
    • :bazel: BackCompat Tests
    • :bazel:🧹 Go mod tidy
    • Linters and static analysis
      • 🍍:lint-roller: Run sg lint → depends on bazel-prechecks
    • Client checks
      • :java: Build (client/jetbrains)
      • :vscode: Tests for VS Code extension
      • :stylelint: Stylelint (all)
    • Security Scanning
      • Semgrep SAST Scan
    • Publish candidate images
      • :bazel::docker: Push candidate Images
    • End-to-end tests
      • :bazel::docker::packer: Executors E2E → depends on bazel-push-images-candidate
    • Publish images
      • :bazel::packer:  Publish executor image → depends on executor-vm-image:candidate
      • :bazel:⤴️ Publish executor binary
      • :bazel::docker: Push final images → depends on main::test main::test_2
    • Release
      • Release tests → depends on bazel-push-images
      • Finalize internal release

```


## Changelog


 <br> Backport 0309564f93 from #64049

Co-authored-by: William Bezuidenhout <william.bezuidenhout@sourcegraph.com>
2024-07-24 19:22:30 +00:00
Will Dollman
fa826c30dc
Bump openjdk-11 version in blobstore image (#64047)
<!-- PR description tips:
https://www.notion.so/sourcegraph/Write-a-good-pull-request-description-610a7fd3e613496eb76f450db5a49b6e
-->
Update the version of openjdk-11 we use in the blobstore image.

We updated the blobstore image earlier today to try and fix this issue,
but were thwarted by an old version pin. This has now been
[removed](https://github.com/sourcegraph/sourcegraph/pull/64045), so
updating packages fully resolves the issue.

This PR branch is not based off `main` as backporting package changes
often results in merge conflicts. It was generated by checking our
`5.5.x` and running `sg wolfi lock blobstore`.

## Test plan

- CI
- Run image locally

<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->

## Changelog

<!-- OPTIONAL; info at
https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c
-->
2024-07-24 16:20:47 +00:00
Release Bot
a88bc4d2d8
[Backport 5.5.x] fix(source): Fix issue where worker would crash if a Bitbucket Cloud token couldn''t be refreshed (#64037)
A Bitbucket Cloud incident caused APIs to error which caused Bitbucket
Cloud OAuth tokens to fail to refresh. This revealed that the Bitbucket
Cloud client called `oauthutil.DoRequest` with a `nil` logger, causing a
nil pointer dereference.

This PR simply creates the logger before calling `DoRequest`, which is
what the other clients do.

## Test plan

No more cases of DoRequest with a nil logger.



## Changelog

- Fixed an issue where a Bitbucket Cloud OAuth token failing to refresh
would crash the `worker` service.


 <br> Backport bc036ad2ba from #64028

Co-authored-by: Petri-Johan Last <petri.last@sourcegraph.com>
2024-07-24 16:22:12 +02:00
sourcegraph-buildkite
d498442148
security: Auto-update package lockfiles for Sourcegraph base images (#64035)
Automatically generated PR to update package lockfiles for Sourcegraph
base images.

Built from Buildkite run
[#283970](https://buildkite.com/sourcegraph/sourcegraph/builds/283970).
## Test Plan
- CI build verifies image functionality

Co-authored-by: Buildkite <buildkite@sourcegraph.com>
2024-07-24 11:46:40 +00:00
Release Bot
0f4cbff0ca
[Backport 5.5.x] Integrate security release approval into release pipeline (#64030)
As part of the [Vuln Scanning
Improvements](https://linear.app/sourcegraph/project/[p0]-vulnerability-scanning-improvements-75299c4312dd/issues)
project, I&#39;ve been working on tooling to automate the security
approval step of the release process.

This PR integrates these improvements into the release pipeline:

* Internal releases will run a vulnerability scan
* Promote-to-public releases will check for security approval

If a public release does not have security approval, it will block the
promotion process. The step happens at the start of the pipeline so
should be a fast-fail. You can also check for release approval before
running promotion by running `@secbot cve approve-release
&lt;version&gt;` in the #secbot-commands channel. In an ideal world we
(security) will have already gone through and approved ahead of release.

I&#39;ve tested this PR as much as I can without running an actual
release! We have a 5.5.x release tomorrow so it&#39;ll be a good test.
If it does cause problems that can&#39;t be easily solved, it can always
be temporarily disabled.

I&#39;ve tagged this PR to be backported to `5.5.x`.



## Pre-merge checklist

- [x] Revert commit that disables release promotion

## Test plan

Manual testing of the release process:
- [x] [Successful test
run](https://buildkite.com/sourcegraph/sourcegraph/builds/283774#0190dfd6-fa70-4cea-9711-f5b8493c7714)
that shows the security scan being triggered
- [x] [Promote to public test
run](https://buildkite.com/sourcegraph/sourcegraph/builds/283826) that
shows the security approval approving a release
- [x] [Promote to public test
run](https://buildkite.com/sourcegraph/sourcegraph/builds/283817#0190e0ec-0641-4451-b7c7-171e664a3127)
that shows the security approval rejecting a release with un-accepted
CVEs



## Changelog


 <br> Backport 9dd901f3c9 from #63990

Co-authored-by: Will Dollman <will.dollman@sourcegraph.com>
2024-07-24 10:42:33 +01:00
Release Bot
1a463ba167
[Backport 5.5.x] [logging] Only record events if a new user was created (#64005)
Follow-up on https://github.com/sourcegraph/sourcegraph/pull/63843

Based on comments from
[this](https://sourcegraph.slack.com/archives/C04RG0JD8L9/p1721668767261719?thread_ts=1721661216.365709&amp;cid=C04RG0JD8L9)
Slack thread, it seems like the events causing the spam are ones where a
new ext acct is saved without a user being created. So if we want to fix
the spam we need to only save an event if a user was created.



## Test plan

Test updated.



## Changelog


 <br> Backport 777c7a0899 from #64004

Co-authored-by: Petri-Johan Last <petri.last@sourcegraph.com>
2024-07-23 13:09:11 +01:00
Release Bot
074af1bda8
[Backport 5.5.x] Publish images for all commits on release branches (#63987)
In order to run nightly vulnerability scans of Sourcegraph releases, we
need to publish a new set of images whenever the release branch is
pushed to.

Previously, this was implemented in
https://github.com/sourcegraph/sourcegraph/pull/63379 but with RFC 795
the release branch format changed from 5.5.1234 to 5.5.x.

This PR updates the regex to catch this new format.

The end result of this is that whenever Buildkite runs on a branch
matching `\d.\d.x`, it will push images to the
`us.gcr.io/sourcegraph-dev/gitserver` registry with the tag
`$branch-insiders`.

I&#39;ve also tagged this PR for backport as we want it on the current
patch release branch 5.5.x :)



## Test plan

- Test buildkite run on branch `will-0.0.x` (with modified regex to
match that branch)
https://buildkite.com/sourcegraph/sourcegraph/builds/283608



## Changelog


 <br> Backport b7242d280f from #63985

Co-authored-by: Will Dollman <will.dollman@sourcegraph.com>
2024-07-22 16:22:32 +00:00
Release Bot
9522c46e6b
[Backport 5.5.x] [fix] Only trigger externalAcctSignup event when a new user is created (#63975)
Currently events are triggered whenever a user signs in with
`http-header` auth. This is because of the `GetAndSaveUser` function
always triggering an event.

However, before the new telemetry events, these events were only created
when a new user was created.

This PR brings the new telemetry code in line with the old telemetry
code to stop the massive amounts of spam caused by this event.

Closes SRC-461

## Test plan

Adjust expected events in unit test.



## Changelog

- Fixed an issue where the `http-header` auth would cause a massive
amount of event logs spam


 <br> Backport cd65951961 from #63843

Co-authored-by: Petri-Johan Last <petri.last@sourcegraph.com>
2024-07-22 13:06:38 +01:00
Camden Cheek
556b880329
Backport: Blame: fix some issues with the stream (#63865) (#63929)
Contributes to SRCH-738

Notably, this does not yet identify the root cause of SRCH-738, but it
does identify and fix some confounding bugs. It's possible that these
actually also _cause_ some of the issues in SRCH-738, but I wanted to at
least push these to dotcom, where we can reproduce some of the
weirdness. At the very least, it doesn't explain the auth errors being
reported.

(cherry picked from commit d91fab39e2)

Co-authored-by: Michael Bahr <michael.bahr@sourcegraph.com>
2024-07-22 10:43:52 +01:00
Michael Lin
3117b03be9
[Backport 5.5.x] Upgrade cody web experimental package to 0.2.7 (#63863) (#63901)
backport https://github.com/sourcegraph/sourcegraph/pull/63863

S2 Cody Web is broken at the moment. New client-config handlers fail
with 401 status because we don't send custom headers, this works for gql
queries since they all are POST requests and the browser automatically
sends an Origin header for them and this is enough for our auth
middleware to check cookies, but with client-config which is rest it's
not the case and we should send `X-Requested-Client: Sourcegraph` header
to make our auth middleware to pass this query correctly

Note that this problem doesn't exist in local builds since we proxy all
requests and add `X-Requested-Client: Sourcegraph` in dev server.

See Cody latest build PR for more details
https://github.com/sourcegraph/cody/pull/4898

## Test plan

CI

Co-authored-by: Vova Kulikov <vovakulikov@icloud.com>
2024-07-18 10:29:02 +01:00
Release Bot
9cf00da25a
[Backport 5.5.x] fix(batches): the baseURL for github instance is now updated when creating a GitHub app (#63833)
Closes SRCH-723

The baseURL for GitHub apps defaults to `https://github.com` when no
`externalServiceURL`, we somehow missed this during our testing.

![CleanShot 2024-07-12 at 11 57
00@2x](https://github.com/user-attachments/assets/99b68a11-de38-4a2d-8c4c-3219f0c9abf7)


## Test plan



Manual testing with the GHE instance.

## Changelog


 <br> Backport 1c40c9e5bc from #63803

Co-authored-by: Bolaji Olajide <25608335+BolajiOlajide@users.noreply.github.com>
Co-authored-by: Anish Lakhwara <anish+github@lakhwara.com>
2024-07-17 18:18:13 +00:00
Varun Gandhi
6b8d334563
Backport 63870 to 5.5.x (#63882)
See  https://github.com/sourcegraph/sourcegraph/pull/63870

cc @sourcegraph/release

## Test plan

Covered by existing tests

## Changelog

- Adds an experimental feature `commitGraphUpdates` to control how
upload visibility is calculated.
2024-07-17 13:45:36 -04:00
Release Bot
21247e44ac
[Backport 5.5.x] Upgrade cody web experimental package to 0.2.5 (#63856)
This PR upgrades the cody web experimental package to 0.2.5, in the new
version we fixed
- Telemetry problem with init extension-related events (we don&#39;t
send install extension events anymore)
- Most recent updates on LLM availability for enterprise instances 
 
## Test plan
- CI is green
- Manual check on basic Cody Web functionality (highly recommended) <br>
Backport e6bd85e4b7 from #63839

Co-authored-by: Vova Kulikov <vovakulikov@icloud.com>
2024-07-17 12:01:11 -04:00
Vova Kulikov
20adc60d67
[Backport-5.5.x]: Move Cody Web to beta (#63806) (#63808)
Closes

https://linear.app/sourcegraph/issue/CODY-2847/change-experimental-labels-to-beta

## Test plan
- Check that the cody web page and cody web side panel have beta badges

(cherry-picked from commit fbb0a1fec1)

## Test plan
- Check that the side-panel Cody and Cody Chat page have beta product
status badges
2024-07-15 12:07:10 -07:00
Release Bot
308624f144
[Backport 5.5.x] Context: return lines around symbol match (#63788)
This PR fixes an important bug in #62976, where we didn&#39;t properly
map the
symbol line match to the return type. Instead, we accidentally treated
symbol
matches like file matches and returned the start of the file.

## Test plan

Add new unit test for symbol match conversion. Extensive manual testing.
<br> Backport 004eb0fd83 from #63773

Co-authored-by: Julie Tibshirani <julietibs@apache.org>
2024-07-11 16:14:52 -04:00
Release Bot
174c08c8c2
[Backport 5.5.x] fix/alertmanager: downgrade prometheus/common to fix generated config (#63793)
The OTEL upgrade https://github.com/sourcegraph/sourcegraph/pull/63171
bumps the `prometheus/common` package too far via transitive deps,
causing us to generate configuration for alertmanager that altertmanager
doesn&#39;t accept, at least until the alertmanager project cuts a new
release with a newer version of `promethues/common`.

For now we forcibly downgrade with a replace. Everything still builds,
so we should be good to go.

## Test plan
`sg start` and `sg run prometheus`. On `main`, editing
`observability.alerts` will cause Alertmanager to refuse to accept the
generated configuration. With this patch, all is well it seems - config
changes go through as expected. This is a similar test plan for
https://github.com/sourcegraph/sourcegraph/pull/63329

## Changelog

- Fix Prometheus Alertmanager configuration failing to apply
`observability.alerts` from site config <br> Backport
ffa873f3ad from #63790

Co-authored-by: Robert Lin <robert@bobheadxi.dev>
2024-07-11 15:49:58 -04:00
Release Bot
8ee41490b9
[Backport 5.5.x] release/bug: generate a new stitched migration graph (#63769)
This will correct6 upgrade path for mvu plan creation

## Test plan

CI test



## Changelog


 <br> Backport cb19d6f0a9 from #63764

Co-authored-by: Warren Gifford <warren@sourcegraph.com>
2024-07-10 22:01:36 +00:00
Release Bot
344169fd47
[Backport 5.5.x] chore(release): bump stitch graph generation (#63768)
Missing bit for the minor release version bump

## Test plan

CI


 <br> Backport 087ad83995 from #63767

Co-authored-by: Jean-Hadrien Chabran <jean-hadrien.chabran@sourcegraph.com>
2024-07-10 21:00:59 +00:00
317 changed files with 14290 additions and 5073 deletions

View File

@ -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 ================================

View File

@ -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,

View File

@ -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}`,
},

View File

@ -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">

View File

@ -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}`)

View File

@ -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,

View File

@ -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>

View File

@ -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}>

View File

@ -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 />

View File

@ -383,6 +383,7 @@ const AddToken: FC<AddTokenProps> = ({
authenticatedUser={user as unknown as AuthenticatedUser}
minimizedMode={true}
kind={kind}
externalServiceURL={externalServiceURL}
/>
)}
</>

View File

@ -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

View File

@ -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>

View File

@ -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,

View File

@ -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

View File

@ -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>

View File

@ -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'>> {

View File

@ -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(),

View File

@ -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
}

View File

@ -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}
/>
)}

View File

@ -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">

View File

@ -15,6 +15,7 @@ export interface SiteAdminSideBarGroupContext extends BatchChangesProps {
isSourcegraphDotCom: boolean
codeInsightsEnabled: boolean
endUserOnboardingEnabled: boolean
applianceUpdateTarget: string
}
export interface SiteAdminSideBarGroup extends NavGroupDescriptor<SiteAdminSideBarGroupContext> {}

View File

@ -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',

View File

@ -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",

View File

@ -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 {

View File

@ -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()

View 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"),
)

View 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
View 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))
}
}

View 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"),
)

View 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
View 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))
}
}

View File

@ -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

View File

@ -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

View File

@ -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(),

View File

@ -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,

View File

@ -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
View File

@ -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",

View File

@ -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",
],
)

View File

@ -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),

View File

@ -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 {

View File

@ -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")

View File

@ -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:

View 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()

View 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

View 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 "$@"

View 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
View File

@ -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

1030
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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")
}
})
}
}

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View 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

View File

@ -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

View File

@ -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
)

View File

@ -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")
}

View File

@ -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")

View File

@ -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

View 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

View 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;
}
}

View File

@ -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>
)
}

View File

@ -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'

View File

@ -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>
)

View File

@ -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'

View File

@ -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}

View File

@ -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>
)
)
}

View File

@ -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" />}

View File

@ -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)

View File

@ -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(() => {

View File

@ -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>
)
}

View File

@ -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()
}
})
}

View File

@ -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)

View 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()
}
})
}

View 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",
],
)

View 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)
}

View 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},
},
}
}

View 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")
}

View File

@ -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
View 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
})
}

View 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)
}
}

View File

@ -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",

View 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.

View File

@ -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 }

View 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
}

View File

@ -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.

View File

@ -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"],
)

View File

@ -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) {

View File

@ -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

View File

@ -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 {

View File

@ -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__"],
)

View File

@ -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()
}

View File

@ -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__"],
)

View File

@ -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")
}

View File

@ -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"`
}

View File

@ -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://&lt;ip-address-above&gt;/</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>

View File

@ -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