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

Dynamic plugins list in the website #658

Merged
merged 9 commits into from
Oct 24, 2020

Conversation

ahmetb
Copy link
Member

@ahmetb ahmetb commented Oct 9, 2020

This relies on Netlify's serverless functions capability to render
plugin count (in the homepage) and plugin list (/docs/plugins) in
the website!

SIG Docs TL has cleared the usage of Functions feature here:
https://groups.google.com/g/kubernetes-sig-docs/c/cXqQm60tGs8

The serverless function is called "api" and it's accessible on path
/.netlify/functions/api/* and has two subpaths that return JSON responses:

  1. */pluginCount
  2. */plugins

This serverless function is built by placing a binary in /site/functions
named "api" and the build instruction is in 'netlify.toml' file.

Since we get plugin list by calling GitHub API, by default we run without
an authentication token. However after this is merged, I will go and add a
permissionless $GITHUB_ACCESS_TOKEN that I have created from my account.

Unauthenticated GitHub calls are limited to 60 per IP, but since I'll add a
personal access token, that should not be an issue. When debugging locally,
60 is also fine. (deploy previews for PRs from non-maintainers will not have
an access token available, so they will make unauthenticated calls to GH API.)

The responses from the dynamic functions are actually cached in Netlify's
CDN and that helps with the rate-limiting as well. I have currently set the
CDN cache duration to 1 hour for each response.

Please take a look at the previews and let me know if there is anything that
doesn't look good.

I might delete generate-plugin-overview tool and update the current
plugins.md in a followup PR.

/assign @chriskim06
/assign @corneliusweig

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Oct 9, 2020
@k8s-ci-robot k8s-ci-robot added approved Indicates a PR has been approved by an approver from all required OWNERS files. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Oct 9, 2020
@ahmetb ahmetb force-pushed the netlify-dynamic-content branch 5 times, most recently from 8f53f0d to bbeed9c Compare October 9, 2020 23:40
@ahmetb
Copy link
Member Author

ahmetb commented Oct 9, 2020

/hold
Not entirely ready for review, requires some setup. I'll ping again later.

@k8s-ci-robot k8s-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 9, 2020
@ahmetb
Copy link
Member Author

ahmetb commented Oct 9, 2020

/hold cancel

Ready to preview at:

Feel free to add commits to my branch.

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 9, 2020
This relies on Netlify's serverless functions capability to render
plugin count (in the homepage) and plugin list (/docs/plugins) in
the website!

The serverless function is called "api" and it's accessible on path
/.netlify/functions/api/* and has two subpaths that return JSON responses:

1. */pluginCount
2. */plugins

This serverless function is built by placing a binary in /site/functions
named "api" and the build instruction is in 'netlify.toml' file.

Since we get plugin list by calling GitHub API, by default we run without
an authentication token. However after this is merged, I will go and add a
permissionless $GITHUB_ACCESS_TOKEN that I have created from my account.

Unauthenticated GitHub calls are limited to 60 per IP, but since I'll add a
personal access token, that should not be an issue. When debugging locally,
60 is also fine. (deploy previews for PRs from non-maintainers will not have
an access token available, so they will make unauthenticated calls to GH API.)

The responses from the dynamic functions are actually cached in Netlify's
CDN and that helps with the rate-limiting as well. I have currently set the
CDN cache duration to 1 hour for each response.

Please take a look at the previews and let me know if there is anything that
doesn't look good.

I might delete `generate-plugin-overview` tool and update the current
plugins.md in a followup PR.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
Copy link
Contributor

@corneliusweig corneliusweig 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 really nice work! A few nits, but otherwise this is perfect stuff.

Very cool that you found out about netlify functions. This will save us so much pointless work.


I agree that generate-plugin-overview should be deleted asap as soon as this is merged.

@@ -11,7 +11,8 @@ Krew helps you:
- install them on your machine,
- and keep the installed plugins up-to-date.

There are [over 90 kubectl plugins][list] currently distributed on Krew.
There are [over <span id="krew-plugin-count">...</span> kubectl plugins][list]
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
There are [over <span id="krew-plugin-count">...</span> kubectl plugins][list]
There are [<span id="krew-plugin-count">...</span> kubectl plugins][list]

IIUC, the number is exact?

fetch('/.netlify/functions/api/pluginCount')
.then(response => response.json())
.then(response => {
console.log(`${response.data.count} plugins found`);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd omit console.log for the good case. We don't want to spam the console.

.then(response => response.json())
.then(response => {
console.log(`${response.data.count} plugins found`);
pluginCountElem.innerText=response.data?.count || 0;
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
pluginCountElem.innerText=response.data?.count || 0;
pluginCountElem.innerText=response.data?.count || '?';

This will make it clearer that there was a failure.

Copy link
Member Author

Choose a reason for hiding this comment

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

Might do <error> :D

@@ -44,5 +44,77 @@
</script>
{{ end }}

<script>
document.addEventListener('DOMContentLoaded', (event) => {
var pluginCountElem = document.getElementById("krew-plugin-count");
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer to use querySelectorAll instead, and replace all occurrences on the page. I can easily imagine that we will show the plugin count in two different places and then we will need that.

.then(response => response.json())
.then(response => {
var count = response.data?.plugins?.length || 0;
console.log(`${count} plugins loaded`);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: no logging?

Comment on lines 197 to 215
for {
select {
case <-ctx.Done():
return
case url, ok := <-queue:
if !ok {
return
}
p, err := readPlugin(url)
if err != nil {
retErr = err
cancel()
return
}
mu.Lock()
out = append(out, p)
mu.Unlock()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

When using errgroup, this could be simplified to

for url := range queue {
  p, err := readPlugin(url)
  if err != nil {
    return err
  }
  mu.Lock()
  out = append(out, p)
  mu.Unlock()
}

Copy link
Member Author

Choose a reason for hiding this comment

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

might use errgroup, but I don't see a way to limit parallelism with errgroup.
feel free to add a patch.

func readPlugin(url string) (*krew.Plugin, error) {
resp, err := http.Get(url)
if err != nil {
return nil, errors.Wrapf(err,"failed to get %s", url)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: run gofmt :)

Comment on lines 239 to 241
if resp.Body != nil {
defer resp.Body.Close()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
if resp.Body != nil {
defer resp.Body.Close()
}
defer resp.Body.Close()

From the spec:

// The http Client and Transport guarantee that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. It is the caller's responsibility to
// close Body.

if resp.Body != nil {
defer resp.Body.Close()
}
var v krew.Plugin
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: move v right before yaml.Unmarshal

Comment on lines 279 to 280
// To debug locally, you can run this server with -port=:8080 and run "hugo serve" and uncomment this:
mux.Handle("/", httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: "localhost:1313"}))
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment reads like this should be commented out?

Anyway, I think it'd be much nicer like that, then no commenting is necessary:

if *port > -1 {
   ...
}

Copy link
Member Author

Choose a reason for hiding this comment

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

It can be commented out but I left it on just in case. You're right.

@ahmetb
Copy link
Member Author

ahmetb commented Oct 12, 2020

@corneliusweig I think it would be better if you do a pass with your commits on the frontend pieces. My frontend skills are weak at best. :)

@corneliusweig
Copy link
Contributor

@ahmetb I added some commits but had to create a PR to your repo: ahmetb#2
Please have a look

@ahmetb
Copy link
Member Author

ahmetb commented Oct 16, 2020

🤔 I see "Allow edits and access to secrets by maintainers" so you should be able to push to my branch. Have you tried over ssh (git://) ?

@corneliusweig
Copy link
Contributor

So I tried again various approaches---nothing will update this PR. However, now there's also a new branch in krew, called netlify-dynamic-content :(.

So I think it's easiest if you merge my PR into your PR 🙄

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
gopkg.in/yaml.v2 v2.2.8
sigs.k8s.io/krew v0.4.0
Copy link
Member Author

Choose a reason for hiding this comment

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

this is a bit worrisome to pin the site to a particular version of master. (should be ok as long as we keep the types/consts backwards compatible)

this replace statement works fine locally, but once I uncommented the replace statement and deploy, it fails to build on Netlify sadly. not sure why.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it will be annoying if we forget about it. I don't see a better alternative though.

Comment on lines +243 to +251
knownHomePages := map[string]string{
`https://krew.sigs.k8s.io/`: "kubernetes-sigs/krew",
`https://sigs.k8s.io/krew`: "kubernetes-sigs/krew",
`https://kubernetes.github.io/ingress-nginx/kubectl-plugin/`: "kubernetes/ingress-nginx",
`https://kudo.dev/`: "kudobuilder/kudo",
`https://kubevirt.io`: "kubevirt/kubectl-virt-plugin",
`https://popeyecli.io`: "derailed/popeye",
`https://soluble-ai.github.io/kubetap/`: "soluble-ai/kubetap",
}
Copy link
Member Author

Choose a reason for hiding this comment

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

@corneliusweig I wonder if we should move this to somewhere like a var declaration at the top. I suspect it'll get updated frequently. :D

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should even go one step further and extract this configuration into non-code. In krew-index-tracker, I also need this list and it is error-prone to redeploy every time this changes.

Maybe we can keep this map in a very dumb format in a text file inside krew-index or krew. Something like

https://krew.sigs.k8s.io/ kubernetes-sigs/krew
https://sigs.k8s.io/krew kubernetes-sigs/krew
...

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm -1 for a new file/format.
I think we can hardcode these still in a centralized source code file.
We can do a variable on top of main.go for now.

@@ -44,5 +44,58 @@
</script>
{{ end }}

<script>
Copy link
Member Author

Choose a reason for hiding this comment

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

@corneliusweig should we move these to a .js file at this point?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that makes sense to me. We don't have to do it in this PR though. Maybe open a new issue? [[main reason: collaboration on the shared PR seems to be not straightforward]]

Copy link
Member Author

Choose a reason for hiding this comment

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

ack

</script>

<script type="module">
import {html, render} from 'https://unpkg.com/lit-html?module';
Copy link
Member Author

Choose a reason for hiding this comment

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

I must admit I don't fully understand how imports work in the browser so @corneliusweig you’ll probably be on point for maintaining these :D I barely can get the vanilla js working.

Copy link
Contributor

@corneliusweig corneliusweig Oct 18, 2020

Choose a reason for hiding this comment

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

Yeah, I'm not too worried about it (support stats).

}

func main() {
port := flag.Int("port", -1, `To debug locally set --port=8080 and run "hugo serve"`)
Copy link
Contributor

Choose a reason for hiding this comment

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

Running the server locally with hugo did not work for me, I always got not found, because the site was trying to query the server on port 1313 (hugo) instead of 8080. Can you write down the instructions in a step-by-step manner?

Copy link
Member Author

Choose a reason for hiding this comment

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

pushed a ./site/functions/README.md . PTAL

try {
const resp = await fetch('/.netlify/functions/api/pluginCount');
const json = await resp.json();
count = json.data?.count ?? '<error>';
Copy link
Member

Choose a reason for hiding this comment

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

It seems like optional chaining is relatively new (or at least those version numbers seem a little high). Do either of you think this should be changed to the old way or is it ok?

Copy link
Contributor

Choose a reason for hiding this comment

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

The question immediately becomes: how far back do we want to be backwards compatible.

This case is trivial, but import and async/await is already more involved and the old style quickly becomes kind of ugly. We could warn page visitors by including a <script nomodule ..> script to inform the user that the page is not working as intended and to switch to a newer browser.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think these are mostly fine. I tend to assume developers use latest browser versions most of the time. In that regard they're more savvy than average computer user.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
@ahmetb
Copy link
Member Author

ahmetb commented Oct 21, 2020

added two more commits:

  1. 543f132 move from /docs/plugins to /plugins → this way plugin list isn't part of docs
  2. fb0b188 add /plugins to navbar → show a "Plugins" item on the top navigation bar.

@corneliusweig
Copy link
Contributor

Oh no! I'm so slow. Apologies 🙇‍♂️
/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Oct 24, 2020
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: ahmetb, corneliusweig

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:
  • OWNERS [ahmetb,corneliusweig]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot merged commit 53aa899 into kubernetes-sigs:master Oct 24, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants