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

Allow Managed clusters to see Secrets Sync Overview and Sidebar nav #26649

Conversation

Monkeychip
Copy link
Contributor

@Monkeychip Monkeychip commented Apr 25, 2024

Note: merging to sidebranch until backend changes are merged. Final screenshots of all views will be added to sidebranch PR.

Description

  • Allows Managed clusters to view Secrets Sync from the sidebar nav and secrets sync overview.
  • Updates badge text to include "Plus" when a user is coming from a managed cluster.
  • Shows only one CTA message regardless of license / managed type.
  • Updates tests.
  • Enterprise tests pass locally.

@Monkeychip Monkeychip added this to the 1.17.0-rc milestone Apr 25, 2024
@github-actions github-actions bot added the hashicorp-contributed-pr If the PR is HashiCorp (i.e. not-community) contributed label Apr 25, 2024
@Monkeychip Monkeychip changed the title Sidebranch: Allow Managed clusters to use Secrets Sync Allow Managed clusters to see Secrets Sync Overview and Sidebar nav Apr 25, 2024
@Monkeychip Monkeychip changed the base branch from main to ui/VAULT-25468/enable-secrets-sync-hvd April 25, 2024 18:32
Copy link

github-actions bot commented Apr 25, 2024

CI Results:
All Go tests succeeded! ✅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In another PR we can decide if we want to change this getter from hasSecretsSync to licenseHasSecretsSync to clarify between HVD vs managed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love that idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the service's getters model params that I pass down to the component. In my mind it read clearer than calling a service each time these are used. However, I understand that one of the benefits of services are they can be called anywhere from within the app. Again, just a preference on my end, but I could be persuaded to not add them as model params and instead call from their services in the templates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

glad you brought this up! i usually prefer to call service getters directly since the state they contain is (generally) global to the application. this way we get the most up-to-date state straight from its source, and have less opportunity for it to become out of date during the passing-down process. in this particular situation i know we don't need to worry about that happening though since we're only fetching flags once at a very high level and that data is unlikely (or impossible) to change.

the way you have it set up here is easier to test though, since we can easily use @licenseHasSecretsSync=true without needing to stub the services that back that up. that's pretty nice. i also see that we're passing properties from the version service down from the route to child components too, and that consistency is great. so, i think your approach works just fine as long as we don't anticipate flags or version getters changing frequently! 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To this point, it's probably a good idea for an acceptance test to cover activating the feature and ensuring the reliant getters update as expected

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed wrapping conditional that would hide the nav item if it was a managed cluster.

@@ -70,7 +70,7 @@ export const PAGE = {
action: (name) => `[data-test-overview-table-action="${name}"]`,
},
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed this to match the other badgeText used on the sync-header component.

Copy link
Contributor

@noelledaley noelledaley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking great so far! glad to see the implementation of this isn't as complicated as we were worried it might be. 🙌

Comment on lines 26 to 32
const isManaged = this.flags.isManaged;
const onLicense = this.version.hasSecretsSync;
const isEnterprise = this.version.isEnterprise;

get syncBadge() {
if (this.version.isCommunity) return 'Enterprise';
if (!this.version.hasSecretsSync) return 'Premium';
if (isManaged) return 'Plus';
if (isEnterprise && !onLicense) return 'Premium';
if (!isEnterprise) 'Enterprise';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is great for readability. nice!

@@ -38,7 +38,10 @@ export default class flagsService extends Service {
return null;
}

// TODO getter will be used in the upcoming persona service
get isManaged() {
return this.managedNamespaceRoot !== null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to futureproof this code i'd recommend swapping the logic here i.e.

get isManaged(): boolean {
   return this.flags && this.flags.includes(FLAGS.vaultCloudNamespace);
}

get managedNamespaceRoot(): string | null {
  return  this.isManaged ? 'admin' : null;
}

^ this makes isManaged more about checking the thing that actually determines if the cluster is managed or not, while managedNamespaceRoot can stay focused on answering "what is the name of the root namespace for this managed cluster?"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smart!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love that idea!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

glad you brought this up! i usually prefer to call service getters directly since the state they contain is (generally) global to the application. this way we get the most up-to-date state straight from its source, and have less opportunity for it to become out of date during the passing-down process. in this particular situation i know we don't need to worry about that happening though since we're only fetching flags once at a very high level and that data is unlikely (or impossible) to change.

the way you have it set up here is easier to test though, since we can easily use @licenseHasSecretsSync=true without needing to stub the services that back that up. that's pretty nice. i also see that we're passing properties from the version service down from the route to child components too, and that consistency is great. so, i think your approach works just fine as long as we don't anticipate flags or version getters changing frequently! 👍

Comment on lines 24 to 32
const isManaged = this.flags.isManaged;
const onLicense = this.version.hasSecretsSync;
const isEnterprise = this.version.isEnterprise;

if (isManaged) return 'Plus feature';
if (isEnterprise && !onLicense) return 'Premium feature';
if (!isEnterprise) 'Enterprise feature';
// no badge for Enterprise clusters with Secrets Sync on their license--the only remaining option.
return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally optional, but if this logic is basically the same as that in components/sidebar/nav/cluster.js it'd be nice to wrap that little badge in a component OR wrap the badge text in a helper used in both places. again, optional, since copying and pasting this in 2 places isn't so bad.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was on the fence about this, and decided to just copy paste the logic. If it was a unique function we could use elsewhere I'd for sure opt for a helper, but this is very sync only.

@Monkeychip Monkeychip marked this pull request as ready for review April 26, 2024 20:18
@Monkeychip Monkeychip requested a review from a team as a code owner April 26, 2024 20:18
Copy link

Build Results:
All builds succeeded! ✅

Copy link
Contributor

@hellobontempo hellobontempo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! These conditionals read clearly and I think this was the right decision on how to handle this logic flow! Just a couple minor comments/suggestions

ui/app/components/sidebar/nav/cluster.js Show resolved Hide resolved
ui/app/components/sidebar/nav/cluster.js Show resolved Hide resolved
}
return null;
get isManaged(): boolean {
return this.flags && this.flags.includes(FLAGS.vaultCloudNamespace);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.flags will always be truthy because the default value is an empty array, right? So I think we can remove the first part of the conditional here. 🤔 If we're concerned that the value passed to setFeatureFlags is undefined then I wonder if a more long-term solution is instead adding a conditional check above so that we can rely on this.flags always being an array☝️

 setFeatureFlags(flags: string[]) {
    this.flags = flags ? flags : [];
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. However, I did some additional moving to make the pattern more consistent and clear in the service how we were setting the tracked property flags (which I now call featureFlags). You'll see I moved the one await fetch call from the application route to this service. Curious if this reads better.

return this.flags && this.flags.includes(FLAGS.vaultCloudNamespace);
}

get managedNamespaceRoot(): string | null {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity - is declaring the return here : string | null necessary? It seems like typescript should be able to infer the return of this getter since it's fairly straight forward

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@noelledaley I'm going to punt this question to you. I probably have the least amount of typescript knowledge on the team. Do you know if this is just a best practice to keep here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! whether or not to explicitly include the return type is somewhat of a stylistic choice, and depends how defensive we want our code to be. 🙂

examples of why you might include the return type:

  • makes the return type more obvious (i.e., no need to hover over the function in VSCode or skim through the code itself)
  • enforces type safety. if we later refactor this function to return undefined instead of null, for example, TS will cause an error and we'll have to decide whether the function should return string | undefined | null or just string | null. it helps us make more intentional decisions.
  • removing the need to hover to find the return type is good for developer accessibility. check out this tremendously awesome article by @ashleemboyer for more.

why you might not include it:

  • you're fine fine hovering/reading the code to figure out the return type
  • you trust TypeScript's ability to infer the correct type

personally, thus far i haven't included return types in functions this small/simple, but as you can see there are benefits to doing so. so, it's up to you!🌟

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much Noelle for the context/conversation. I'll leave in this case per personal preference.

ui/app/services/flags.ts Show resolved Hide resolved
Comment on lines 24 to 32
const isManaged = this.flags.isManaged;
const onLicense = this.version.hasSecretsSync;
const isEnterprise = this.version.isEnterprise;

if (isManaged) return 'Plus feature';
if (isEnterprise && !onLicense) return 'Premium feature';
if (!isEnterprise) return 'Enterprise feature';
// no badge for Enterprise clusters with Secrets Sync on their license--the only remaining option.
return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments here as the get badgeText() above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To this point, it's probably a good idea for an acceptance test to cover activating the feature and ensuring the reliant getters update as expected

Copy link
Contributor

@noelledaley noelledaley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coupla test changes here, then all good!


test('it should render CTA copy and action when feature exists on enterprise license and is activated', async function (assert) {
await render(hbs`<Secrets::LandingCta @isActivated={{true}} @hasSecretsSync={{true}} /> `, {
test('it should render CTA copy and action feature is activated', async function (assert) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test('it should render CTA copy and action feature is activated', async function (assert) {
test('it should render CTA copy and action if feature is activated', async function (assert) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the catch!

this.isActivated = true;
this.licenseHasSecretsSync = true;
this.isManged = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.isManged = false;
this.isManaged = false;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like we also need to add tests for when isManaged is true or licenseHasSecretsSync is false, right? just want to make sure we're accounting for all possible situations that this conditional checks for 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. Let me add those tests in.

const store = this.owner.lookup('service:store');
this.destinations = await store.query('sync/destination', {});
this.destinations = await this.store.query('sync/destination', {});
// component test so set values that would normally be calculated on the route model from the services.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think this comment is necessary since stubbing properties like this is standard for component tests 👍

return this.isManaged ? 'admin' : null;
}

getFeatureFlags = keepLatestTask(async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I moved the fetch from the beforeModel hook in the application route to this concurrency task. It follows the pattern of getActivatedFeatures. However, there is a small difference in my pattern and I wanted to point that out. Compare the try/catch here to the previous call:

// previous call
const result = await fetch('/v1/sys/internal/ui/feature-flags', {
      method: 'GET',
    });
    if (result.status === 200) {
      const body = await result.json();
      const flags = body.feature_flags || [];
      this.flagsService.setFeatureFlags(flags);
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i considered suggesting this change earlier so i'm glad you brought this up. i think it keeps the service's scope nicely organized. i like that this ensures we don't have to manually call this.flags.setFeatureFlags outside of the context of the service. 😀

setFeatureFlags(flags: string[]) {
this.flags = flags;
}
@tracked featureFlags: string[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another great change for readability, love it 👏

Copy link
Contributor

@noelledaley noelledaley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is looking great! the only blocking item i see left is to fix the test here 🎉

return this.isManaged ? 'admin' : null;
}

getFeatureFlags = keepLatestTask(async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i considered suggesting this change earlier so i'm glad you brought this up. i think it keeps the service's scope nicely organized. i like that this ensures we don't have to manually call this.flags.setFeatureFlags outside of the context of the service. 😀

@@ -194,8 +193,8 @@ module('Acceptance | sync | overview', function (hooks) {
this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => {
assert.strictEqual(
req.requestHeaders['X-Vault-Namespace'],
'admin',
'Request is made to admin namespace'
undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am fairly sure that this request is supposed to be made to the admin namespace for HVD clusters since HVD users only have admin access (and not root level).

to resolve that, you'll need to add the namespace to the request here:

.ajax('/v1/sys/activation-flags/secrets-sync/activate', 'POST', { namespace: null });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good catch. Yes this is the POST on activate that we do need to add the namespace. Got myself mixed up.

Copy link
Contributor

@noelledaley noelledaley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well done Angel! this looks fantastic.

@Monkeychip Monkeychip merged commit 73439b9 into ui/VAULT-25468/enable-secrets-sync-hvd Apr 29, 2024
23 checks passed
@Monkeychip Monkeychip deleted the ui/VAULT-26004/secerts-sync-visible-hvd branch April 29, 2024 22:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
hashicorp-contributed-pr If the PR is HashiCorp (i.e. not-community) contributed pr/no-changelog ui
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants