Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugins): adopt vault secret picker #269

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
"@kong-ui-public/entities-gateway-services": "^3.3.0",
"@kong-ui-public/entities-key-sets": "^3.2.0",
"@kong-ui-public/entities-keys": "^3.2.0",
"@kong-ui-public/entities-plugins": "^5.1.0",
"@kong-ui-public/entities-plugins": "^8.7.1",
"@kong-ui-public/entities-routes": "^3.2.0",
"@kong-ui-public/entities-shared": "^3.2.0",
"@kong-ui-public/entities-shared": "^3.6.2",
"@kong-ui-public/entities-snis": "^3.2.0",
"@kong-ui-public/entities-upstreams-targets": "^3.2.0",
"@kong-ui-public/entities-vaults": "^3.3.0",
"@kong-ui-public/forms": "^3.1.5",
"@kong-ui-public/entities-vaults": "^3.9.2",
"@kong-ui-public/forms": "^4.3.1",
"@kong-ui-public/i18n": "^2.2.0",
"@kong-ui-public/misc-widgets": "^2.1.0",
"@kong/icons": "^1.12.0",
Expand Down
1 change: 1 addition & 0 deletions src/pages/plugins/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
:config="config"
:plugin-type="pluginType"
:plugin-id="pluginId"
enable-vault-secret-picker
@error:fetch-schema="onFetchSchemaError"
@update="onSave"
/>
Expand Down
15 changes: 15 additions & 0 deletions tests/playwright/fixtures/vaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

export interface Vault extends Record<string, any> {
id?: string
name: string
prefix: string
config?: Record<string, any>
}

export const VAULTS: Vault[] = [
{ name: 'env', prefix: 'my-env-1', config: { prefix: 'super_secret_1_' } },
{ name: 'env', prefix: 'my-env-2', config: { prefix: 'super_secret_2_' } },
{ name: 'env', prefix: 'my-env-3', config: { prefix: 'super_secret_3_' } },
{ name: 'env', prefix: 'my-env-4', config: { prefix: 'super_secret_4_' } },
]
9 changes: 9 additions & 0 deletions tests/playwright/helpers/vaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createKongResource } from '@pw/commands/createKongResource'
import { VAULTS } from '@pw/fixtures/vaults'

export const createVaults = async () => {
// Create vaults to test the vault secret picker
for (const vault of VAULTS) {
await createKongResource('/vaults', vault)
}
}
155 changes: 154 additions & 1 deletion tests/playwright/pages/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Page } from '@playwright/test'
import { expect, type Locator, type Page } from '@playwright/test'
import { VAULTS } from '@pw/fixtures/vaults'
import { POM } from '.'

export class PluginListPage extends POM {
Expand All @@ -10,3 +11,155 @@ export class PluginListPage extends POM {
super(page, '/plugins')
}
}

export class PluginFormPage extends POM {
public $ = {
...POM.$,
}

constructor (page: Page, plugin: string) {
super(page, `/plugins/${plugin}/create`)
}

/**
* Get the field's form group with a given label
*
* @param labelFor the `for` attribute of the <label /> element of a field
* @returns Locator
*/
locateFormGroup (labelFor: string, searchIn?: Locator) {
return (searchIn ?? this.page.locator('.kong-ui-entities-plugin-form'))
.locator('.form-group')
.filter({ has: this.page.locator(`label[for="${labelFor}"]`) })
}

/**
* Wait for request to /vaults (for vault secret picker)
*
* @returns Promise to wait
*/
waitForVaults () {
return this.page.waitForRequest(
(res) => res.method() === 'GET' && /\/vaults($|\?.*)/.test(res.url()),
)
}

get vaultSecretPickerVaultSelect () {
return this.page.getByTestId('vault-secret-picker-vault-select')
}

get vaultSecretPickerSecretIdSelect () {
return this.page.getByTestId('vault-secret-picker-secret-id-select')
}

get vaultSecretPickerSecretIdInput () {
return this.page.getByTestId('vault-secret-picker-secret-id-input')
}

get vaultSecretPickerSecretKeyInput () {
return this.page.getByTestId('vault-secret-picker-secret-key-input')
}

async testVaultSecretPicker (wrapper: Locator, input?: Locator) {
await wrapper.scrollIntoViewIfNeeded()

// Find the vault secret picker action and open the picker
const vspAction = wrapper.locator('.vault-secret-picker-provider')
.locator('.vault-secret-picker-provider-action')

// Expect request
await Promise.all([vspAction.click(), this.waitForVaults()])

// Find the modal
await expect(this.page.getByTestId('vault-secret-picker-modal').locator('.modal-container'))
.toBeVisible()

// Select a vault (env-2)
await this.vaultSecretPickerVaultSelect.click()
await expect(
this.page.getByTestId('vault-secret-picker-vault-popover')
.locator('.select-items-container .select-item'),
).toHaveCount(VAULTS.length)

await this.page.getByTestId('select-item-my-env-2').click()

// Manually input secret ID and key
await expect(this.vaultSecretPickerSecretIdSelect).toHaveCount(0) // not exist

await expect(this.vaultSecretPickerSecretIdInput).toBeEnabled()
await this.vaultSecretPickerSecretIdInput.fill('passwords')

await expect(this.vaultSecretPickerSecretKeyInput).toBeEnabled()
await this.vaultSecretPickerSecretKeyInput.fill('admin')

// Proceed
await this.page.getByTestId('modal-action-button').click()

const inputLocator = input ?? wrapper.locator('input')

await expect(inputLocator).toHaveValue('{vault://my-env-2/passwords/admin}')

// Try a non-existing vault
await inputLocator.clear()
await inputLocator.fill('{vault://404-vault/secrets/super-secret}')

// Open the picker
await Promise.all([vspAction.click(), this.waitForVaults()])

// Should not fill in any input fields
await expect(this.vaultSecretPickerVaultSelect).toBeEnabled()
await expect(this.vaultSecretPickerVaultSelect).toHaveValue('')

await expect(this.vaultSecretPickerSecretIdSelect).toHaveCount(0)
await expect(this.vaultSecretPickerSecretIdInput).toBeDisabled()
await expect(this.vaultSecretPickerSecretIdInput).toHaveValue('')

await expect(this.vaultSecretPickerSecretKeyInput).toBeDisabled()
await expect(this.vaultSecretPickerSecretKeyInput).toHaveValue('')

// Close the modal
await this.page.getByTestId('modal-cancel-button').click()

// Try without secret ID and key
await inputLocator.clear()
await inputLocator.fill('{vault://my-env-3}')

// Open the picker
await Promise.all([vspAction.click(), this.waitForVaults()])

// Should only have the vault select filled
await expect(this.vaultSecretPickerVaultSelect).toBeEnabled()
await expect(this.vaultSecretPickerVaultSelect).toHaveValue('my-env-3')

await expect(this.vaultSecretPickerSecretIdSelect).toHaveCount(0)
await expect(this.vaultSecretPickerSecretIdInput).toBeEnabled()
await expect(this.vaultSecretPickerSecretIdInput).toHaveValue('')

await expect(this.vaultSecretPickerSecretKeyInput).toBeEnabled()
await expect(this.vaultSecretPickerSecretKeyInput).toHaveValue('')

// Close the modal
await this.page.getByTestId('modal-cancel-button').click()

// Try an invalid secret reference
await inputLocator.clear()
await inputLocator.fill('{vault://') // missing '}'

// Open the picker
await Promise.all([vspAction.click(), this.waitForVaults()])

// Should not fill in any input fields
await expect(this.vaultSecretPickerVaultSelect).toBeEnabled()
await expect(this.vaultSecretPickerVaultSelect).toHaveValue('')

await expect(this.vaultSecretPickerSecretIdSelect).toHaveCount(0)
await expect(this.vaultSecretPickerSecretIdInput).toBeDisabled()
await expect(this.vaultSecretPickerSecretIdInput).toHaveValue('')

await expect(this.vaultSecretPickerSecretKeyInput).toBeDisabled()
await expect(this.vaultSecretPickerSecretKeyInput).toHaveValue('')

// Close the modal
await this.page.getByTestId('modal-cancel-button').click()
}
}
19 changes: 18 additions & 1 deletion tests/playwright/specs/plugins/01-Plugins.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { waitAndDismissToasts } from '@pw/commands/waitAndDismissToast'
import { withNavigation } from '@pw/commands/withNavigation'
import { getPropertyValue } from '@pw/commands/getPropertyValue'
import { ConsumerListPage } from '@pw/pages/consumers'
import { PluginListPage } from '@pw/pages/plugins'
import { PluginFormPage, PluginListPage } from '@pw/pages/plugins'
import { RouteListPage } from '@pw/pages/routes'
import { ServiceListPage } from '@pw/pages/services'
import { createVaults } from '@pw/helpers/vaults'

const mockConsumerName = 'testUser'
const mockRouteName = 'testRoute'
Expand All @@ -40,6 +41,8 @@ test.describe('plugins', () => {
await clearKongResources('/services')
await clearKongResources('/consumers')
await clearKongResources('/plugins')
await clearKongResources('/vaults')
await createVaults()
})

test.beforeEach(async ({ pluginListPage }) => {
Expand All @@ -51,6 +54,7 @@ test.describe('plugins', () => {
await clearKongResources('/services')
await clearKongResources('/consumers')
await clearKongResources('/plugins')
await clearKongResources('/vaults')
})

test('plugin list should be empty now', async ({ page }) => {
Expand All @@ -60,6 +64,19 @@ test.describe('plugins', () => {
await expect(emptyState).toContainText('Configure a New Plugin')
})

// This step only test for basic functionality of the vault secret picker.
// Find more tests in specs-plugins.
test.only('should show vault secret picker under eligible fields', async ({ page }) => {
const formPage = new PluginFormPage(page, 'http-log')

await formPage.goto()

const httpEndpointFormGroup = formPage.locateFormGroup('config-http_endpoint')

await expect(httpEndpointFormGroup.locator('.vault-secret-picker-provider')).toBeVisible()
await formPage.testVaultSecretPicker(httpEndpointFormGroup)
})

test('install a plugin when the scope is "service"', async ({ page, serviceListPage }) => {
await createKongResource('/services', {
name: mockServiceName,
Expand Down
Loading